How to Build Game UI with HTML and CSS Overlays

Updated July 2026
HTML and CSS overlays are the most practical way to build game UI for web games. By positioning DOM elements on top of the game canvas, you get the browser's full rendering capabilities for free: text shaping, responsive layout, CSS transitions, accessibility support, and native input handling. This guide walks through the complete process of setting up an HTML overlay system, routing input between the UI and game layers, building common UI components, and keeping everything performant.

The HTML overlay approach is used by the majority of production web games because it separates UI concerns from game rendering. The game engine handles the canvas, rendering the game world, processing physics, and running game logic. The DOM layer handles the UI, displaying menus, health bars, dialogue boxes, and notifications. JavaScript bridges the two layers by passing game state to the UI and player input back to the game. This separation makes both layers easier to develop, debug, and maintain.

Step 1: Set Up the Overlay Structure

The foundation is a container element that holds both the game canvas and the UI overlay. The container uses CSS position: relative to establish a positioning context. The canvas fills the container. The UI overlay also fills the container but sits on top via position: absolute and a higher z-index.

The HTML structure is a wrapper div containing the canvas element and a UI div: the wrapper is position: relative; width: 100%; height: 100%, the canvas is display: block; width: 100%; height: 100%, and the UI overlay is position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10. The z-index value does not matter as long as the UI layer is higher than the canvas.

If the game runs fullscreen, the container should expand to fill the viewport using width: 100vw; height: 100vh or by applying the Fullscreen API to the container element (not just the canvas, which would hide the UI overlay). When fullscreening only the canvas, the DOM overlay disappears because it is outside the fullscreened element. Always fullscreen the container that holds both layers.

The UI overlay div acts as the root for all UI elements. HUD elements, menu panels, dialogue boxes, and notification toasts are all children of this overlay div. Using a single root for all UI makes it easy to show/hide the entire UI layer (for cutscenes or screenshots) and provides a scoping boundary for CSS styles.

Step 2: Handle Input Event Routing

The most critical technical challenge with HTML overlays is input routing. The UI overlay div covers the entire canvas, so by default it captures all click and touch events, preventing the game from receiving input. The solution is CSS pointer-events: none on the overlay div, with pointer-events: auto on individual UI elements that should receive clicks.

Setting pointer-events: none on the overlay root makes all events pass through to the canvas below. Then, each interactive UI element (buttons, sliders, clickable panels) explicitly sets pointer-events: auto so it captures its own events. Non-interactive elements (labels, decorative frames, background panels) inherit the pointer-events: none from the root and remain transparent to input.

Keyboard input does not need routing because keyboard events fire on the document or window regardless of which element is visually on top. The game's keyboard handler listens on the document, and the UI's keyboard handler listens on specific input elements (like text fields). If the game and UI both use the same keys (Escape for both pause menu and closing a sub-menu), the UI handler should call event.stopPropagation() when it handles the event, preventing it from reaching the game handler.

Touch and scroll events require special attention. A scrollable UI element (like an inventory list) inside the overlay will capture touch events for scrolling, but if the touch starts outside the scrollable area, it should pass through to the canvas. The pointer-events approach handles this correctly as long as the scrollable container has pointer-events: auto and the overlay root has pointer-events: none. Add overscroll-behavior: contain on scrollable UI elements to prevent scroll events from propagating to the page.

Step 3: Build HUD Elements in HTML

HUD elements built in HTML are standard DOM elements with CSS styling. A health bar is a container div with a child div whose width is set as a percentage. A score counter is a span element whose textContent is updated when the score changes. An ability icon is an img or div with a background-image and an overlay for the cooldown indicator.

Position HUD elements using CSS absolute positioning within the overlay container. Use percentage-based offsets for responsive positioning: top: 2%; left: 2% for the health bar, top: 2%; right: 2% for the minimap, bottom: 2%; left: 50%; transform: translateX(-50%) for a centered ability bar. These positions adapt to any viewport size without JavaScript.

CSS custom properties (variables) create a consistent design system for HUD elements. Define colors, sizes, and spacing as variables on the overlay root: --hud-bg: rgba(0, 0, 0, 0.5); --hud-text: #ffffff; --hud-accent: #4af; --hud-gap: 8px; --hud-radius: 4px. Every HUD element references these variables, so changing the visual theme requires updating only the variable definitions.

Update HUD elements from the game loop, but only when values change. In each frame, check if the game state has changed since the last update: if (player.health !== lastDisplayedHealth) { healthFill.style.width = (player.health / player.maxHealth * 100) + '%'; lastDisplayedHealth = player.health; }. This dirty-checking prevents unnecessary DOM writes, which is important because even simple DOM writes trigger style recalculation.

For animated HUD elements like smoothly draining stamina bars, use CSS transitions rather than per-frame JavaScript updates. Set transition: width 0.2s ease-out on the bar fill element, and simply set the new width. The browser handles the smooth interpolation on the GPU, producing smoother animation at lower CPU cost than JavaScript-driven animation.

Step 4: Create Menu Overlays

Menu overlays are UI panels that appear on top of the game when the player opens a menu. A menu overlay consists of a backdrop (a semi-transparent dark layer that covers the entire viewport) and the menu panel itself (centered or positioned within the viewport).

The backdrop serves two functions: it visually separates the menu from the game by dimming the background, and it captures clicks outside the menu panel. Implement the backdrop as a div with position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); pointer-events: auto. Clicking the backdrop should close the menu (if appropriate) or do nothing. The menu panel sits inside or above the backdrop with its own styling and pointer-events.

Menu show/hide uses CSS transitions for smooth appearance. Adding and removing a CSS class that toggles opacity (from 0 to 1) and visibility (from hidden to visible) creates a fade transition. For slide-in menus, transition the transform property from translateY(20px) to translateY(0). The transition duration should be 150 to 250ms. Use will-change: opacity, transform on the menu panel to promote it to a GPU layer for smoother transitions.

When a menu opens, the game should pause. The game loop checks a paused flag before processing game logic and rendering. The menu's open function sets this flag, and the close function clears it. For web games, pausing also means stopping requestAnimationFrame callbacks or setting the game loop to only render the static paused state without advancing game time.

Focus management is important for menu accessibility. When a menu opens, move keyboard focus to the first interactive element in the menu using element.focus(). When the menu closes, return focus to the element that was focused before the menu opened. This ensures keyboard and screen reader users can interact with the menu without mouse input. A focus trap (preventing Tab from moving focus outside the menu while it is open) improves the experience further.

Step 5: Synchronize State Between Canvas and DOM

The game engine and the DOM UI are two separate systems that need to share data. The game engine knows the player's health, score, inventory contents, and game state. The DOM UI needs to display this data. The player clicks UI buttons that need to trigger game actions. A synchronization mechanism bridges the two layers.

The simplest approach is a shared state object. Both the game engine and the UI code reference a plain JavaScript object containing the current game state: health, score, level, inventory array, and any other displayable data. The game engine writes to this object during its update loop. The UI reads from this object during its update cycle (which can be slower than the game loop, perhaps 10 to 20 times per second for non-critical displays).

An event-based approach provides better decoupling. The game engine emits custom events (using a simple EventEmitter pattern or CustomEvent on the document) when state changes: emit('healthChanged', { current: 75, max: 100 }). The UI subscribes to these events and updates the relevant DOM elements. This approach updates the UI only when data actually changes, eliminating the need for dirty-checking in the UI layer.

For UI actions that affect the game (button clicks that trigger abilities, menu selections that change settings), the UI dispatches events or calls functions on the game engine's public API. A "use potion" button click handler might call game.useItem('health-potion') or emit emit('useItem', { id: 'health-potion' }). The game engine processes the action in its next update tick, and the resulting state change propagates back to the UI through the state sync mechanism.

Timing matters for synchronization. The game loop runs at 60fps (every 16.67ms). DOM updates should not happen every frame unless the data changes every frame. Batch DOM writes at the end of the frame, after all game logic has completed, so the browser can process layout and paint in a single pass. Reading DOM layout properties (offsetWidth, getBoundingClientRect) during the game loop forces synchronous layout recalculation and should be avoided.

Step 6: Optimize Rendering Performance

DOM-based UI adds rendering cost on top of the game's canvas rendering. The browser must process style calculations, layout, paint, and compositing for the DOM layer in addition to rendering the canvas. Keeping this cost low ensures the UI does not consume frame budget that the game needs.

Minimize DOM complexity. Fewer elements mean less layout work. A health bar needs two divs (container and fill), not five (container, border, fill, glow effect, text label). If a glow effect is desired, use CSS box-shadow rather than a separate element. If a text label is optional, add it only when the player hovers rather than keeping a hidden element in the DOM.

Use CSS transforms for animation instead of layout properties. Animating transform and opacity uses the GPU compositor and does not trigger layout recalculation. Animating width, height, top, or left triggers layout recalculation for the element and potentially its siblings. A sliding notification should animate transform: translateX() rather than left. A scaling button press should animate transform: scale() rather than width and height.

Batch DOM reads and writes. Reading a DOM property (like offsetWidth) forces the browser to complete any pending layout calculations. Writing to a DOM property (like style.width) invalidates the layout. Alternating reads and writes in a loop creates "layout thrashing" where the browser recalculates layout multiple times per frame. Instead, read all needed values first, then write all updates. Libraries like fastdom automate this batching.

Profile UI rendering cost using Chrome DevTools' Performance tab. Record a few seconds of gameplay and examine the "Rendering" section of the flame chart. Look for long "Recalculate Style," "Layout," or "Paint" blocks during frames. If these blocks push the total frame time above 16ms, identify which DOM updates are causing them and optimize or eliminate those updates. Common culprits are: updating innerHTML (forces full element reparsing), reading offsetWidth during animation (forces synchronous layout), and animating properties that trigger paint (background-color, border-color, box-shadow changes).

Key Takeaway

HTML/CSS overlays are the most practical UI technology for web games because they leverage the browser's built-in rendering, layout, and accessibility systems. The key technical challenges, input routing with pointer-events, state synchronization between canvas and DOM, and rendering performance, each have well-established solutions that scale to production complexity.