Browser games have a unique advantage: you can use both HTML/CSS and canvas rendering for your interface. Most native game engines force you to build UI inside the game renderer. In the browser, you get the full power of CSS — flexbox, grid, transitions, media queries — layered on top of your canvas.
The question isn’t which one to use. It’s which one to use for what.
A good HUD shows only what the player needs, when they need it. Here are patterns that work well in browser games:
Place persistent info in screen corners using CSS position: fixed:
<div id="hud" style="position:fixed;inset:0;pointer-events:none;z-index:10">
<div class="hud-tl">Health / Mana</div>
<div class="hud-tr">Minimap</div>
<div class="hud-bl">Hotbar / Tools</div>
<div class="hud-br">Gold / Day counter</div>
</div>
<style>
.hud-tl { position:absolute; top:12px; left:12px; }
.hud-tr { position:absolute; top:12px; right:12px; }
.hud-bl { position:absolute; bottom:12px; left:12px; }
.hud-br { position:absolute; bottom:12px; right:12px; }
[class^="hud-"] { pointer-events:auto; }
</style>
The pointer-events: none on the container lets clicks pass through to the game canvas, while pointer-events: auto on the actual HUD elements makes them interactive.
For a farming game, resource bars might show energy, hydration, or crop health:
.bar-container {
width: 120px; height: 14px;
background: rgba(0,0,0,.5);
border-radius: 7px;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: linear-gradient(90deg, #e74c3c, #f39c12);
transition: width 0.3s ease;
border-radius: 7px;
}
/* Update via JS: barFill.style.width = `${hp/maxHp*100}%` */
Don’t show everything at once. Show the watering can tooltip only when hovering over a crop, the sell price only when near the shipping bin, the build menu only in build mode. Use a state machine to toggle HUD panels:
function updateHUD(gameState) {
document.getElementById('build-panel').hidden = gameState.mode !== 'build';
document.getElementById('shop-panel').hidden = !gameState.nearShop;
document.getElementById('tool-tip').hidden = !gameState.hoveredEntity;
}
Games need nested menus: Main Menu → Settings → Audio. Use a stack pattern:
const menuStack = [];
function pushMenu(menuId) {
if (menuStack.length) {
document.getElementById(menuStack.at(-1)).hidden = true;
}
menuStack.push(menuId);
document.getElementById(menuId).hidden = false;
}
function popMenu() {
const current = menuStack.pop();
document.getElementById(current).hidden = true;
if (menuStack.length) {
document.getElementById(menuStack.at(-1)).hidden = false;
}
}
// Escape key pops the stack
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && menuStack.length) popMenu();
});
When the pause menu opens, stop the game loop but keep the last frame visible behind a semi-transparent overlay:
#pause-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,.7);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
Your game runs on 27-inch monitors and 5-inch phones. The canvas scales, but the UI needs to adapt:
/* Desktop: multi-column inventory */
.inventory-grid {
display: grid;
grid-template-columns: repeat(8, 48px);
gap: 4px;
}
/* Mobile: fewer columns, bigger slots */
@media (max-width: 600px) {
.inventory-grid {
grid-template-columns: repeat(5, 56px);
gap: 6px;
}
.hud-tl, .hud-tr, .hud-bl, .hud-br {
transform: scale(1.25);
transform-origin: inherit;
}
}
Tooltips in games need to follow the cursor (or touch point) and stay within viewport bounds:
function positionTooltip(tooltip, x, y) {
const pad = 12;
const rect = tooltip.getBoundingClientRect();
let left = x + pad;
let top = y + pad;
// Clamp to viewport
if (left + rect.width > window.innerWidth) left = x - rect.width - pad;
if (top + rect.height > window.innerHeight) top = y - rect.height - pad;
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
}
For touch devices, show tooltips on long-press rather than hover. A 300ms press-and-hold is the standard mobile convention.
Font choice affects both feel and performance:
-apple-system, sans-serif) load instantly and look native. Good for UI text.Always use font-display: swap in your @font-face to prevent invisible text while the font loads.
Game UI is notoriously inaccessible. Small changes make a big difference:
tabindex and :focus-visible styles.--ui-scale CSS variable applied to the HUD container works well.aria-live="polite" on elements that update dynamically (gold count, notifications) so screen readers announce changes.| UI Element | Best Approach | Why |
|---|---|---|
| Main menu | HTML/CSS | Text, buttons, accessibility |
| In-game HUD | HTML overlay | Easy to update, CSS transitions |
| Floating health bars | Canvas | Moves with game camera |
| Minimap | Canvas | Redraws every frame |
| Inventory grid | HTML/CSS | Drag-and-drop, responsive layout |
| Dialog/speech bubbles | HTML overlay | Rich text, auto-sizing |
FarmHeart uses all of these patterns. Open DevTools and inspect the HUD.
Play FarmHeart