Skip to content

Stage 2 — CSS and Game Styling

In Stage 2 you write all the CSS for the game. There is no game logic yet — this stage is entirely visual. By the end you will have the dark gradient canvas, the HUD positioned at the top, and the Start screen looking like it belongs.


  • The complete style.css for the game

Checkpoint: open index.html — the canvas should show a deep blue-to-purple gradient, the “Bubble Pop” title should appear in a violet gradient, and the Play button should be styled.


Every browser applies default margins, padding, and box-sizing rules that will interfere with a full-viewport game. Start with a reset:

*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: #0a0a1a;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Segoe UI', system-ui, sans-serif;
color: #fff;
overflow: hidden;
}
  • box-sizing: border-box — padding and borders are included in element dimensions, not added on top.
  • display: flex on body — centres the game container both horizontally and vertically on any screen.
  • overflow: hidden — prevents scrollbars from appearing when a bubble reaches the edge.

The container is a fixed-size rectangle. The canvas fills it completely.

#game-container {
position: relative;
width: 480px;
height: 640px;
}
#game-canvas {
display: block;
width: 100%;
height: 100%;
border-radius: 12px;
background: linear-gradient(180deg, #0d1b4b 0%, #1a0d3b 100%);
box-shadow: 0 0 40px rgba(100, 60, 220, 0.4);
}
  • position: relative on the container — establishes a positioning context for the HUD and screens, which will be position: absolute.
  • width: 100% on the canvas — stretches the canvas CSS display to fill the container. The actual pixel grid (canvas.width / canvas.height) was set in JS at 480 × 640. CSS stretches or shrinks the displayed result without changing the pixel grid.
  • background on the canvas — visible before the first JS draw call and between frames while the canvas is cleared.

The HUD shows score, level, and lives across the top of the canvas.

#hud {
position: absolute;
top: 12px;
left: 0;
width: 100%;
display: flex;
justify-content: space-between;
padding: 0 16px;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.04em;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
pointer-events: none;
}
  • position: absolute — floats the HUD on top of the canvas. It is positioned relative to #game-container (the nearest positioned ancestor).
  • pointer-events: none — allows clicks to pass through the HUD to the canvas underneath, so clicking a bubble near the HUD still registers.

Both overlay screens share a .screen class. The game-over screen also starts with .hidden.

.screen {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 16px;
border-radius: 12px;
background: rgba(10, 10, 26, 0.85);
backdrop-filter: blur(4px);
text-align: center;
padding: 32px;
}
.screen.hidden {
display: none;
}
  • inset: 0 — shorthand for top: 0; right: 0; bottom: 0; left: 0. Stretches the screen to fill the container.
  • backdrop-filter: blur(4px) — blurs what is behind the overlay, giving a frosted-glass effect.
  • .screen.hidden { display: none } — toggled by JS to show and hide screens. When a screen is hidden, it is completely removed from layout and not interactive.

.screen h1 {
font-size: 56px;
font-weight: 800;
letter-spacing: -0.02em;
background: linear-gradient(135deg, #a78bfa, #60a5fa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
}
.screen h2 {
font-size: 42px;
font-weight: 800;
background: linear-gradient(135deg, #f87171, #fb923c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
#high-score-display,
#final-score {
font-size: 20px;
color: #c4b5fd;
}
#new-high-score {
font-size: 18px;
font-weight: 700;
color: #fbbf24;
animation: pulse 0.8s ease-in-out infinite alternate;
}
#new-high-score.hidden {
display: none;
}
.instructions {
font-size: 15px;
color: #94a3b8;
max-width: 280px;
}

The gradient text technique clips the gradient to the text shape using background-clip: text and makes the text itself transparent with -webkit-text-fill-color: transparent. It requires both the prefixed and unprefixed versions for browser compatibility.


button {
margin-top: 8px;
padding: 14px 48px;
font-size: 18px;
font-weight: 700;
border: none;
border-radius: 50px;
cursor: pointer;
background: linear-gradient(135deg, #7c3aed, #2563eb);
color: #fff;
box-shadow: 0 4px 20px rgba(124, 58, 237, 0.5);
transition: transform 0.1s, box-shadow 0.1s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 28px rgba(124, 58, 237, 0.65);
}
button:active {
transform: translateY(0);
}

The border-radius: 50px on a wide button creates a pill shape. The transform: translateY(-2px) on hover gives a subtle lift effect.


The “New High Score!” message uses a keyframe animation:

@keyframes pulse {
from { opacity: 0.7; }
to { opacity: 1; }
}

Open index.html. You should see:

  • A dark blue-to-purple gradient canvas with a soft purple glow
  • The “Bubble Pop” title in a violet-to-blue gradient
  • “Best: 0” in light purple below the title
  • Instructions text in muted grey
  • A purple-to-blue pill button labelled “Play”

The HUD at the top shows “Score: 0 Level: 1 Lives: 3” but there is no layout yet — it overlaps the start screen. That is fine. Once the game starts (Stage 3), the start screen will be hidden and the HUD will be visible on its own.


In Stage 3 you will implement the game loop using requestAnimationFrame, populate the POP.config object with all game settings, and write the Bubble class that draws gradient bubbles on the canvas.