Lesson 03

Mobile controls: make the game playable without blocking the game.

A browser game is not finished when it works with WASD on a desktop keyboard. Players also tap, drag, rotate phones, open comments and type nicknames. Input code has to respect all of that.

IntermediateTouchAccessibility20-30 minutes

1. Do not steal keyboard input from forms

Supagames has ratings and comments. That means game pages can contain textareas. If a global key handler captures Space or arrow keys everywhere, users cannot type normal comments. The fix is an editable-target guard.

function editable(target) {
    if (!target) return false;
    const tag = (target.tagName || "").toLowerCase();
    return tag === "input" || tag === "textarea" || target.isContentEditable;
}

document.addEventListener("keydown", function(event) {
    if (editable(event.target)) return;

    if (event.key === " " || event.key === "ArrowUp") {
        event.preventDefault();
        jump();
    }
});

This pattern fixed a real usability bug: players could open the comment modal, but Space was still treated as a game action instead of a typed character.

2. Use pointer events for touch buttons

Pointer events work with mouse, touch and stylus. A simple mobile control layer can use `data-key` attributes to map buttons to the same key state as the keyboard.

<div class="touch-controls" aria-label="Touch controls">
    <button data-key="ArrowLeft">Left</button>
    <button data-key="ArrowRight">Right</button>
    <button data-action="attack">Attack</button>
</div>
function setupTouchControls() {
    const buttons = document.querySelectorAll("[data-key], [data-action]");

    buttons.forEach(function(button) {
        const key = button.getAttribute("data-key");
        const action = button.getAttribute("data-action");

        function down(event) {
            event.preventDefault();
            if (key) keys[key] = true;
            if (action === "attack") primaryAttack();
        }

        function up(event) {
            event.preventDefault();
            if (key) keys[key] = false;
        }

        button.addEventListener("pointerdown", down);
        button.addEventListener("pointerup", up);
        button.addEventListener("pointercancel", up);
        button.addEventListener("pointerleave", up);
    });
}

Source pattern: Level 33 action core.

3. Keep controls out of the play area

Mobile buttons should never cover the only place where the player needs to see enemies, hazards or quest targets. A good approach is to reserve a bottom control strip or make buttons translucent and outside the main action lane.

canvas {
    touch-action: none;
}

.touch-controls {
    position: fixed;
    left: 0.75rem;
    right: 0.75rem;
    bottom: 0.75rem;
    display: flex;
    justify-content: space-between;
    pointer-events: none;
}

.touch-controls button {
    pointer-events: auto;
    min-width: 64px;
    min-height: 64px;
    border-radius: 18px;
    opacity: 0.82;
}

The `pointer-events` trick lets the wrapper ignore accidental taps while buttons still work. It also helps when the game canvas fills most of the screen.

4. One input system, many devices

The cleanest pattern is a shared `keys` object. Keyboard events and touch buttons both update the same state. Your movement logic does not care whether the player pressed a real key or touched a screen button.

const keys = {};

function updatePlayer() {
    player.vx = 0;

    if (keys.ArrowLeft || keys.KeyA) player.vx -= player.speed;
    if (keys.ArrowRight || keys.KeyD) player.vx += player.speed;

    player.x += player.vx;
}

This prevents duplicate desktop and mobile movement code. Duplicate input code is a quiet source of bugs because one path gets fixed and the other keeps the old behavior.

5. Pause input when overlays are open

Game pages often contain more than the game. Supagames pages can show instructions, ratings, comment forms and share panels. If those overlays are open, gameplay keys should usually stop controlling the character. Otherwise a player can lose health while trying to type, vote or close a modal.

let inputLocked = false;

function openCommentModal() {
    inputLocked = true;
    commentDialog.hidden = false;
    commentTextarea.focus();
}

function closeCommentModal() {
    commentDialog.hidden = true;
    inputLocked = false;
}

document.addEventListener("keydown", function(event) {
    if (inputLocked || editable(event.target)) return;
    keys[event.code] = true;
});

The important idea is not the exact variable name. The important idea is that your page has UI state outside the canvas. Once you accept that, controls become more respectful and fewer bugs appear when you add community features later.

6. Design touch controls for thumbs

A phone player is not using a mouse cursor. They cover part of the screen with both hands. Put movement on the left, actions on the right and keep the center of the screen clear for enemies, roads, platforms or puzzle pieces.

Button size matters too. Tiny buttons may look elegant in a screenshot, but they fail during a real game. Aim for at least 56 to 64 CSS pixels for action buttons. Add visual feedback on press, because mobile players need confirmation that the browser received the touch.

.touch-controls button:active,
.touch-controls button.is-down {
    transform: translateY(2px) scale(0.98);
    filter: brightness(1.25);
}

Do not hide essential UI behind the phone controls. If a health bar, timer or objective is near the bottom, move it above the controls or place the controls in a separate strip.

Checklist before publishing

  • Can the game be played with keyboard only?
  • Can it be played with touch only?
  • Does Space stop page scrolling during gameplay?
  • Can the user type spaces in comments, search fields and nickname fields?
  • Do touch buttons avoid covering enemies, exits, quest text and important UI?
Next: Build collision helpers you can trust. Later: Add enemies and objectives that make a game last longer.