DEV Community

footballbros.me operator
footballbros.me operator

Posted on

Lessons from Running a Small HTML5 Sports-Game Site: Embedding, Performance and Mobile Controls

Hosting browser games looks simple: drop an <iframe> on a page and you're done. In practice, a game frame is one of the heaviest, most input-hungry things you can put in a document, and it fights with the page around it in surprising ways.

On football bros, our site that hosts the game Football Bros along with roughly 35 other HTML5 football and soccer games, we've run into most of those fights. A quick disclaimer up front: we didn't make Football Bros. It's developed by Blue Wizard Digital (official site: footballbros.io), and we're an independent site with no affiliation. Our job is the boring-but-important layer around the games: embedding, page performance, input handling and keeping things usable on phones.

Here's what we've learned, with code you can reuse.

1. Embed third-party games defensively

A game iframe runs someone else's code. Treat it that way.

The sandbox attribute starts from "deny everything" and lets you opt back in to specific capabilities. Most HTML5 games need scripts and, frequently, same-origin access to their own storage (for save data or settings). Popups and top-level navigation are rarely needed and are a common source of unwanted redirects.

The allow attribute (Permissions Policy) controls powerful features such as fullscreen, autoplay and gamepad access.

<div class="game-wrapper">
  <iframe
    id="game-frame"
    title="Football Bros – browser game"
    data-src="https://example-game-host.com/football-bros/index.html"
    sandbox="allow-scripts allow-same-origin allow-pointer-lock"
    allow="fullscreen; autoplay; gamepad"
    allowfullscreen
    loading="lazy"
    referrerpolicy="strict-origin-when-cross-origin"
  ></iframe>
  <button class="play-btn" type="button">Play</button>
</div>
Enter fullscreen mode Exit fullscreen mode

A few notes:

  • allow-scripts + allow-same-origin together effectively let a same-origin frame remove its own sandbox. That's only acceptable when the game is served from a different origin than your site, which is one more reason to keep game files on a separate domain or subdomain.
  • Add allow-popups only if you need it. Some games link out to their developer's site; if you allow popups, consider allow-popups-to-escape-sandbox carefully.
  • Always set a title. Screen readers announce it, and it's the only description an iframe gets.
  • allowfullscreen is the legacy attribute; allow="fullscreen" is the modern one. Including both costs nothing.

2. Don't load the game until someone wants to play

A game bundle can easily be several megabytes of JavaScript, WebAssembly, audio and textures. If it loads on page load, it competes with everything else for bandwidth and main-thread time, which hurts Largest Contentful Paint (LCP) and Interaction to Next Paint (INP).

loading="lazy" on iframes helps for frames below the fold, but on a game page the frame is usually above the fold, so the browser loads it immediately anyway. The more effective pattern is click-to-play: render a lightweight poster image and a button, and only set the iframe's src when the user asks for it.

const wrapper = document.querySelector('.game-wrapper');
const frame = document.getElementById('game-frame');
const playBtn = wrapper.querySelector('.play-btn');

playBtn.addEventListener('click', () => {
  frame.src = frame.dataset.src;   // start loading the game now
  playBtn.hidden = true;
  frame.focus();                   // give keyboard input to the game
});

// Optional fullscreen toggle using the Fullscreen API
function toggleFullscreen() {
  if (!document.fullscreenElement) {
    wrapper.requestFullscreen?.().catch(console.warn);
  } else {
    document.exitFullscreen();
  }
}
Enter fullscreen mode Exit fullscreen mode

Benefits we've seen in practice:

  • The poster image becomes the LCP element, and it's small and cacheable.
  • Visitors who only came to read instructions or browse other games don't pay the download cost.
  • Audio autoplay policies stop being a problem, because the game starts after a user gesture.

Call requestFullscreen() on the wrapper, not the iframe, if you want your own overlay controls (like an exit button) to stay visible. Note that iOS Safari has historically had limited Fullscreen API support for non-video elements, so always feature-detect and keep a usable non-fullscreen layout. MDN's Fullscreen API guide is the best reference for the current quirks.

3. Stop arrow keys and Space from scrolling the page

This is the number one complaint on any game page: you press the down arrow to move a player and the whole page scrolls instead.

When the iframe has focus, key events go to the game document, and the game should call preventDefault() itself. But focus is fragile. Users click outside the frame, tab around, or the game never grabs focus to begin with. Then arrow keys and Space go to your page and scroll it.

Two mitigations:

  1. Focus the iframe on play (shown above), and re-focus it when the user clicks the game area.
  2. Suppress scroll keys on the host page while a game is active, but never while the user is typing in a form field.
const SCROLL_KEYS = new Set([
  'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' ', 'PageUp', 'PageDown'
]);
let gameActive = false;

playBtn.addEventListener('click', () => { gameActive = true; });

window.addEventListener('keydown', (e) => {
  if (!gameActive || !SCROLL_KEYS.has(e.key)) return;
  const t = e.target;
  const typing = t.isContentEditable ||
    ['INPUT', 'TEXTAREA', 'SELECT'].includes(t.tagName);
  if (!typing) e.preventDefault();
}, { passive: false });
Enter fullscreen mode Exit fullscreen mode

Be deliberate here. Hijacking Space and arrows on a page breaks keyboard scrolling for people who rely on it. Only enable it once a game is actually running, and give users an obvious way out (an "Exit game" button that resets gameActive).

4. Touch input: touch-action and responsive containers

On mobile, the default browser behaviors that hurt games are swipe-to-scroll, pinch-zoom and double-tap-zoom. Inside the game's own document, touch-action: none on the canvas is the game developer's job. On the host page, you can still make the frame area behave better:

.game-wrapper {
  position: relative;
  width: 100%;
  max-width: 960px;
  margin-inline: auto;
  aspect-ratio: 16 / 9;        /* reserves space: no layout shift */
  background: #111 url(/img/poster.webp) center / cover no-repeat;
  touch-action: none;          /* no pan/zoom gestures over the game */
  overscroll-behavior: contain;
}

.game-wrapper iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

@media (orientation: portrait) and (max-width: 600px) {
  .game-wrapper { aspect-ratio: 9 / 16; max-height: 80svh; }
}
Enter fullscreen mode Exit fullscreen mode

The aspect-ratio property replaced the old padding-top: 56.25% hack and, more importantly, reserves the frame's space before anything loads. That keeps Cumulative Layout Shift (CLS) near zero even when the game boots late. Different games ship with different native ratios, so we store a ratio per game rather than forcing 16:9 everywhere.

Also: if a game only works with a keyboard, say so on the page. A mobile visitor discovering that after a 10 MB download is a bad experience, and it's a cheap fix.

5. Cache game assets aggressively (and correctly)

Game files are ideal for long-lived caching, as long as filenames are versioned (game.3f9a1c.js). A reasonable setup:

  • Hashed assets (JS, WASM, sprites, audio): Cache-Control: public, max-age=31536000, immutable
  • The game's index.html and any unhashed loader: Cache-Control: no-cache (revalidate every time, so updates roll out).
  • WASM must be served as application/wasm for streaming compilation to work.
  • Enable Brotli or gzip for JS, JSON and WASM, but skip it for already-compressed formats like PNG, WebP, MP3 and OGG.

A CDN in front of the game origin makes a huge difference for international visitors, and putting games on a separate host also keeps their heavy assets from sharing a connection pool with your page's critical resources.

6. Basic accessibility still applies

Games themselves vary widely in accessibility, and you can't fix third-party code. You can make the surrounding page solid:

  • Use a real <button> for Play and Fullscreen, not a clickable <div>.
  • Give each iframe a descriptive title.
  • Write out the controls as text ("Arrow keys to move, Space to kick") instead of only showing an image.
  • Make sure focus is visible and that users can tab out of the game area.
  • Respect prefers-reduced-motion in your own page animations.

Wrapping up

Most of the work in hosting HTML5 games isn't about the games. It's about making a heavy, third-party, input-hungry iframe coexist with a normal web page. Sandbox it, load it on demand, reserve its space, manage focus and scroll keys carefully, cache its assets well, and keep the page around it accessible. None of this is exotic, but getting it all right is the difference between a page that feels broken and one that just lets people play.

If you host embedded games or interactive widgets yourself, I'd love to hear how you handle focus and mobile input in the comments.

Top comments (1)

Collapse
 
susanacharya profile image
Susan Acharya •

good call flagging allow-scripts + allow-same-origin together, a lot of embed guides skip that. the click-to-load play button is a nice win for page speed too, an iframe that never loads costs nothing