Mobile and touch readiness for HTML5 games on portals
Make an HTML5 game work on phones inside a portal iframe: viewport, pointer input, touch-action, scroll lock, orientation, safe areas and fullscreen.
Some portals make mobile support a requirement. Poki’s quality requirements, for example, ask games to cover the full screen on mobile and to force touch controls on tablets. This guide covers the browser side: input, stopping the page from moving, sizing, orientation and fullscreen, with the limits MDN documents as of 2026-09-30.
Viewport meta tag
Put this in your index.html:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
viewport-fit=cover lets the page extend under notches and rounded corners, which you then handle with safe-area insets (below). MDN’s viewport page warns about the old habit of adding user-scalable=no and maximum-scale. iOS 10 and later ignore both by default, and disabling zoom blocks low-vision users. Stop the game surface from zooming with touch-action instead, and leave the rest of the page alone.
Use Pointer Events for input
Pointer events give you one event model for mouse, pen and touch, with pointerType telling you which one fired. MDN’s Touch events page recommends pointer events when you need to support both touch and mouse.
// Track every active pointer (fingers, mouse, pen) in canvas coordinates.
const canvas = document.querySelector('#game');
const pointers = new Map();
function toCanvas(e) {
const r = canvas.getBoundingClientRect();
return { x: e.clientX - r.left, y: e.clientY - r.top, type: e.pointerType };
}
canvas.addEventListener('pointerdown', (e) => {
canvas.setPointerCapture(e.pointerId); // keep receiving moves off-canvas
pointers.set(e.pointerId, toCanvas(e));
});
canvas.addEventListener('pointermove', (e) => {
if (pointers.has(e.pointerId)) pointers.set(e.pointerId, toCanvas(e));
});
for (const type of ['pointerup', 'pointercancel']) {
canvas.addEventListener(type, (e) => pointers.delete(e.pointerId));
}
canvas.addEventListener('contextmenu', (e) => e.preventDefault()); // long-press menu
setPointerCapture() sends later events for that pointer to the canvas until pointerup or an explicit release. That keeps a virtual joystick working when the thumb slides past the edge. Always handle pointercancel: MDN’s touch-action page says the browser fires it when it takes over a gesture such as panning. Without that handler, a finger can stay “down” in your input state after the browser has taken over.
To switch control schemes, look at pointerType on the last pointerdown: show touch controls after a "touch" event and keyboard hints after "mouse".
If you keep Touch events
Calling preventDefault() in a touch handler only works if the listener is not passive. MDN’s addEventListener page notes that browsers other than Safari default passive to true for touchstart and touchmove on window, document and document.body. Pass { passive: false } explicitly on those targets.
Stop the page from scrolling, zooming or jumping
A game inside a portal’s iframe sits in a longer page, so stray gestures and keys can move that page. Four fixes cover most cases:
html, body {
margin: 0;
height: 100%;
overflow: hidden;
overscroll-behavior: none; /* no scroll chaining or pull-to-refresh */
}
#game {
display: block;
width: 100vw;
height: 100vh; /* fallback */
height: 100dvh;
touch-action: none; /* the game handles every gesture */
-webkit-user-select: none;
user-select: none; /* no text selection on long press */
}
touch-action: nonedisables browser panning and zooming on the canvas. The browser works out the allowed gestures from the touched element and its ancestors, so setting it on the game container is enough. MDN warns that it can block zooming for low-vision users, so apply it to the game surface, not the whole site.overscroll-behavioronly applies to scroll containers, and MDN says an<iframe>is not one. To stop scroll chaining out of your iframe, the overscroll-behavior page says to set it on both<html>and<body>of the framed document, as above.user-select: nonestops long presses from selecting text. Keep the-webkit-user-selectline: MDN’s compatibility data lists only the prefixed form for released Safari on iOS and macOS (unprefixed support appears only in Safari Technology Preview).- Keys. Space and arrow keys scroll the parent page when the game does not handle them. Poki’s HTML5 SDK guide raises this and suggests a keydown handler that cancels the default action. A version limited to your game keys:
const GAME_KEYS = new Set([' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']);
window.addEventListener('keydown', (e) => {
if (GAME_KEYS.has(e.key)) e.preventDefault();
});
window.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
Drop the wheel line if your game has scrollable UI, such as a long shop list.
Size the canvas for the real screen
100dvh tracks the dynamic viewport as mobile browser toolbars appear and disappear. MDN’s length units page explains that plain vh corresponds to the large viewport, which can hide content behind browser UI.
With viewport-fit=cover, keep buttons inside the safe area using env(safe-area-inset-*):
.hud {
position: absolute;
top: max(8px, env(safe-area-inset-top));
right: max(8px, env(safe-area-inset-right));
bottom: max(8px, env(safe-area-inset-bottom));
left: max(8px, env(safe-area-inset-left));
pointer-events: none; /* let taps reach the canvas */
}
.hud button { pointer-events: auto; }
A canvas drawn at CSS-pixel resolution looks blurry on high-density screens. devicePixelRatio gives the ratio of physical to CSS pixels, often above 2 on phones, and page zoom changes it. devicePixelContentBoxSize gives the exact device-pixel size, but MDN’s compatibility data lists no Safari support. This helper uses it where available and falls back to CSS size times devicePixelRatio:
// Keep the canvas backing store matched to its displayed size in device pixels.
export function fitCanvas(canvas, onResize, { maxDpr = Infinity } = {}) {
const observer = new ResizeObserver((entries) => {
const entry = entries.find((e) => e.target === canvas);
if (!entry) return;
const box = entry.contentBoxSize?.[0] ??
{ inlineSize: entry.contentRect.width, blockSize: entry.contentRect.height };
const cssW = box.inlineSize;
const cssH = box.blockSize;
if (cssW === 0 || cssH === 0) return; // hidden or not laid out yet
const dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
const exact = maxDpr === Infinity ? entry.devicePixelContentBoxSize?.[0] : undefined;
const w = exact ? exact.inlineSize : Math.round(cssW * dpr);
const h = exact ? exact.blockSize : Math.round(cssH * dpr);
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
}
onResize(w, h, cssW, cssH);
});
try {
observer.observe(canvas, { box: 'device-pixel-content-box' });
} catch {
observer.observe(canvas); // browsers that reject this box option
}
return () => observer.disconnect();
}
// 2D usage: draw in CSS pixels, let the transform map them to device pixels.
const ctx = canvas.getContext('2d');
fitCanvas(canvas, (w, h, cssW, cssH) => {
ctx.setTransform(w / cssW, 0, 0, h / cssH, 0, 0);
});
// WebGL usage: call gl.viewport(0, 0, w, h) in the callback instead.
The canvas needs a CSS width and height (as in the stylesheet above). Otherwise changing canvas.width changes its layout size and the observer fires again. Setting canvas.width also resets the 2D context to its default state, which is why the transform is reapplied on every call. On a weak GPU you can pass { maxDpr: 2 } to trade sharpness for fill rate. That is a judgement call to test on your slowest target device.
Orientation
Reading orientation is widely available: screen.orientation.type and its change event are listed for Safari 16.4 and later in MDN’s data. Locking is not. From the ScreenOrientation compatibility data, lock() is unsupported in Safari on macOS and iOS and always throws NotSupportedError in desktop Chrome. Chrome for Android supports it, and Firefox lists support from version 144. The lock() page adds that locking is typically only enabled on mobile devices in fullscreen.
So treat locking as a bonus and always ship a fallback overlay:
async function tryLockLandscape() {
try {
await screen.orientation.lock('landscape'); // usually needs fullscreen first
return true;
} catch {
return false; // unsupported or not allowed: rely on the overlay
}
}
.rotate-hint { display: none; }
@media (orientation: portrait) and (pointer: coarse) {
.rotate-hint { display: flex; } /* "Turn your device sideways" */
}
(pointer: coarse) limits the hint to devices whose primary pointer is imprecise, such as a finger. The orientation media feature compares viewport width and height, not the device. MDN notes that opening the on-screen keyboard in portrait can switch it to landscape. Pause the game while the hint is visible. Better still, support both orientations if your layout allows it.
Fullscreen
requestFullscreen() needs transient user activation, so call it from a tap or click handler. Inside an iframe it also depends on the embedding page: the Fullscreen API page explains that the fullscreen permission defaults to self, so a cross-origin frame needs allow="fullscreen" from the portal. Check document.fullscreenEnabled before showing your own fullscreen button, and check the portal first: CrazyGames’ gameplay requirements prohibit custom in-game fullscreen buttons because the site provides fullscreen itself.
On iPhone, do not count on it. MDN’s compatibility data for requestFullscreen marks iOS Safari as partial. It works only on iPad, not iPhone, shows an overlay button that cannot be disabled, and exits on a downward swipe, which the note calls unsuitable for some uses such as games.
fullscreenButton.hidden = !document.fullscreenEnabled;
fullscreenButton.addEventListener('click', async () => {
try {
await document.documentElement.requestFullscreen();
await tryLockLandscape();
} catch {
// Denied or unsupported: the game still fills its frame.
}
});
What to check before you submit
Test on real devices, including at least one iPhone and one Android phone, inside a page that scrolls (an iframe in a long test page will do):
- Dragging, pinching and double-tapping on the game never scroll or zoom the host page.
- Space and the arrow keys do not scroll the host page. Nor does the mouse wheel, unless the game has scrollable UI.
- A finger sliding off the canvas does not leave an input stuck down, and
pointercancelis handled. - Long-pressing does not select text or open a context menu.
- HUD buttons stay clear of notches and home indicators in both orientations.
- The canvas is sharp on a high-density screen and still hits its frame rate on your slowest phone.
- Rotating the device mid-game resizes or pauses correctly. With the keyboard open, the rotate hint does not trap the player.
- The game is fully playable without fullscreen and without an orientation lock.
- Mobile requirements on the target portal’s page are met: see the portal comparison, for example Poki and YouTube Playables.
- Audio starts after the first tap: see the Web Audio unlock guide.
Limits of this guide
Browser support statements come from MDN pages and MDN’s browser-compat-data as of 2026-09-30. Mobile browsers change quickly, so recheck the linked tables before relying on a version number. The code samples have not been verified on physical iOS or Android devices for this guide. How each portal’s iframe grants fullscreen or handles orientation is not covered, because it is set by the portal’s page rather than your game.
Questions
- Can an HTML5 game go fullscreen on an iPhone?
- MDN's compatibility data says the Fullscreen API in iOS Safari is available on iPad only, not iPhone. Design the game to fill the iframe or viewport without it.
- Can I force landscape orientation in a web game?
- Only in some browsers. ScreenOrientation.lock() is not supported in Safari and throws NotSupportedError in desktop Chrome, so pair it with a rotate-your-device overlay.
Sources
- <meta name="viewport"> (MDN)
- Pointer events (MDN)
- Touch events (MDN)
- Element: setPointerCapture() method (MDN)
- touch-action (MDN)
- EventTarget: addEventListener(), passive option (MDN)
- overscroll-behavior (MDN)
- user-select (MDN)
- CSS length units: viewport units (MDN)
- env() (MDN)
- Window: devicePixelRatio property (MDN)
- HTMLCanvasElement: width property (MDN)
- ResizeObserverEntry: devicePixelContentBoxSize property (MDN)
- ResizeObserverEntry compatibility data (mdn/browser-compat-data on GitHub)
- ScreenOrientation: lock() method (MDN)
- ScreenOrientation compatibility data (mdn/browser-compat-data on GitHub)
- user-select compatibility data (mdn/browser-compat-data on GitHub)
- pointer media feature (MDN)
- orientation media feature (MDN)
- Element: requestFullscreen() method (MDN)
- Fullscreen API (MDN)
- Element compatibility data, requestFullscreen (mdn/browser-compat-data on GitHub)
- Gameplay requirements: fullscreen (CrazyGames docs)
- HTML5 SDK: preventing page scroll (Poki for Developers)
- Quality requirements (Poki for Developers)