Poki reported over 100 million players every month in December 2025, across a catalog of more than 1,000 curated games that play in the browser.

Nobody installed anything to play them. Each game is a pile of static files: an HTML page that hosts a canvas element, and JavaScript that runs a loop to draw every frame. No plugin, no download step, just whatever graphics APIs the browser already has, Canvas 2D and WebGL included.

That setup is why download size and browser support end up deciding more about a project than the code does. A game that feels fine on a laptop can still stall on a cheap phone.

What Is HTML Game Development?

“HTML game” and “HTML5 game” mean the same thing. The 5 refers to the standard revision that made the canvas element part of HTML, and plenty of people just skip it.

These games run in a web browser on HTML, CSS, JavaScript and the browser’s graphics APIs. HTML hosts the page, the canvas element and any menu overlay, and CSS handles where that overlay sits and how it looks.

Everything that moves lives in JavaScript. Game state, input, collision checks and the draw calls that paint each frame all sit there.

A static page with an embedded video or animation doesn’t count, since no game logic runs. Neither does a native app that ships its own runtime and installer.

Canvas, WebGL, WebGPU or the DOM: Which Rendering Method Fits Your Game?

Pick by the heaviest frame the game will ever draw. Most sprite games get by fine on Canvas 2D. Shader effects, big sprite counts and 3D push you toward WebGL, and WebGPU makes sense when the work is compute-heavy. Card games and other UI-heavy ones can stay in the DOM.

MethodBest forMain limit
Canvas 2DPlatformers, puzzles, arcade gamesSlows as draw calls pile up
WebGLMany sprites, shaders, 3DContext can be lost on mobile
WebGPUCompute shaders, heavy 3DUneven platform coverage
DOM or SVGCard games, board games, menusLayout cost grows with element count

Start with the simplest method that survives your worst frame. A memory-match game runs on the DOM and a platformer on Canvas 2D. A bullet-heavy shooter wants WebGL through a framework, while a 3D scene needs WebGL or WebGPU through an engine.

Canvas 2D

Canvas 2D works in immediate mode. The game clears the surface and redraws every object on every frame.

MDN Web Docs warns that performance drops once a site pushes the Canvas API to its limits, and the fixes it lists first are cheap ones. Pre-render repeating objects on an offscreen canvas. Stack several canvas elements as layers for background, game and UI. Draw at whole-pixel coordinates to avoid sub-pixel rendering. And if nothing needs transparency, request an opaque context with alpha set to false.

WebGL and WebGPU

Where is web design headed next?

Discover the latest web design statistics: industry growth, design trends, technology adoption, and insights defining the future of the web.

Explore the Data →

The Chrome team’s November 2025 announcement on web.dev lists WebGPU in Chrome and Edge 113 (on Windows, macOS and ChromeOS), Firefox 141 and Safari 26.

Coverage still varies by platform. Chrome added Android in version 121. Firefox 141 ships it on Windows, and macOS on ARM64 arrived with Firefox 145. Linux support in Chromium browsers, and Linux, Android and Intel-based Mac support in Firefox, were still in progress at the time of that post.

WebGL stays the safer base, and the libraries treat it that way.

Three.js documents a WebGL 2 fallback inside its WebGPURenderer and still recommends WebGLRenderer for pure WebGL 2 applications (three.js manual). PixiJS switched its default renderer from WebGPU back to WebGL in v8.1.0 after reports of inconsistent behavior across browsers (PixiJS release notes).

Read together, the sources point one way. Ship WebGPU only behind a WebGL fallback and let the engine pick the backend. Defaults can change between releases, so check the one in the version you install.

DOM and SVG

Card games, board games and menu-driven games rarely need a canvas.

The DOM gives them text rendering, CSS transitions and accessibility for free. SVG adds resolution-independent shapes that stay sharp on any screen density.

The limit is element count. Every moving piece is a DOM node, so a scene full of animated elements pushes layout and style work onto each frame.

How Does the Game Loop Work in the Browser?

Once per display frame, the loop reads input, updates state and draws the scene, then asks the browser for the next frame with requestAnimationFrame.

Developers call that cycle a JavaScript game loop, and every other system in the game hangs off it.

On a canvas, each pass clears and repaints the scene, which is the mechanism behind canvas animations.

MDN documents a few behaviors of requestAnimationFrame that catch people out. Callbacks generally match the display refresh rate, most often 60 Hz, though 75, 120 and 144 Hz are also widely used. Most browsers pause calls in background tabs and hidden iframes. The call is one-shot, so the callback has to request the next frame itself.

MDN’s game loop guide adds an ordering rule. Schedule the next frame before running the loop body, so the browser still receives the request when a frame misses its vsync window.

Delta time for variable frame rates

MDN warns that animation runs faster on high refresh-rate screens unless the timestamp passed to the callback drives the movement.

The arithmetic is blunt. A sprite that moves 5 pixels per callback travels 300 pixels per second at 60 Hz and 720 at 144 Hz.

Delta time fixes it. Subtract the previous timestamp from the current one, then multiply speed in pixels per second by that gap in seconds.

Clamp the result. Background tabs pause the loop, so the first frame after a return can carry a gap of many seconds, and without a ceiling the player teleports through a wall.

Fixed timestep for physics and collisions

Delta time works for movement. Collision detection and physics need a fixed timestep.

The loop accumulates elapsed time and runs the update in equal slices (1/60 of a second is common), then draws once. With variable steps, a fast object can cross a thin hitbox between two frames and never register a hit.

A variable delta is simple and fine for movement and animation. A fixed timestep gives you repeatable physics, but it needs an accumulator and a cap on updates per frame, so one slow frame can’t trigger a spiral of catch-up updates.

Phaser, PixiJS, Three.js or Vanilla JavaScript: Which Should You Pick?

Phaser suits a complete 2D game, and PixiJS suits a project that only needs a fast renderer. Three.js or Babylon.js handle 3D. For a first small game, plain JavaScript is enough.

ToolTypeBest for
Phaser2D game frameworkPlatformers, arcade and casual games
PixiJS2D rendering libraryRendering-heavy games, interactive media
Three.js3D libraryCustom 3D scenes with your own structure
Babylon.js3D game engineFull 3D games
Vanilla JavaScriptNo dependencyFirst games, tiny games, learning

2D frameworks

Phaser launched version 4 in April 2026, a ground-up rebuild of its WebGL renderer that keeps the Phaser 3 API (Phaser release notes).

The Canvas renderer still ships but is deprecated, according to the Phaser 4 rendering documentation, so new projects target WebGL.

The appeal is how much comes bundled: the game loop, scenes, input, audio and Arcade physics, plus a large community and plugin ecosystem. The downsides are real, though. Custom WebGL pipelines from version 3 must be rewritten as render nodes, and you learn a whole framework before you get to the game itself.

PixiJS draws and leaves the rest to you. Physics, audio and scene management are your own choices.

Version 8 made renderer creation asynchronous, so code copied from older tutorials needs an await before the renderer exists (PixiJS v8 launch post).

It renders 2D fast, has WebGL and WebGPU backends, and the surface to learn is small. It also gives you no game structure at all, which is the whole point and also the catch.

3D engines

  • Three.js is a 3D library with a WebGPURenderer and a WebGL 2 fallback, and the game structure is yours to build.
  • Babylon.js is a full 3D engine that tries to create a WebGL2 context by default, per its documentation.
  • PlayCanvas comes at it web-first, built on WebGL2 and WebGPU.

When vanilla JavaScript is enough

A first game does not need an engine.

Writing the loop, input and collision code yourself teaches what every framework hides, and a small game ships with zero dependencies.

Stay vanilla while the game is one screen, the objects number in the dozens, and collision is rectangle against rectangle. Once you need scene management, sprite-sheet animation, tilemaps or physics beyond simple overlap tests, a framework starts paying for itself.

Add-ons close small gaps without a full framework. Matter.js covers 2D rigid-body physics and Howler.js handles audio playback. Construct 3 sits at the other end, building game logic from visual events instead of code.

How to Build a First HTML Game Step by Step

Everything starts in a terminal with Vite.

  1. Run npm create vite@latest and pick the vanilla template. Vite requires Node.js 20.19+ or 22.12+ (Vite documentation), and its dev server serves index.html at localhost:5173.
  2. Put one canvas element in index.html, set its width and height attributes, and fetch the 2D context in your script module.
  3. Call requestAnimationFrame, schedule the next frame first, then update and draw using the timestamp.
  4. Let key events set flags in a keys object, and have the update step read those flags. Never move entities inside an event handler.
  5. Clear the canvas, then draw background, entities and score in that order. Keep draw functions free of state changes.
  6. Test axis-aligned hitboxes (rectangles) on every update. Add circles or polygons only when rectangles feel wrong.
  7. Give the menu, playing and game over states their own update and draw routines, plus one reset function that rebuilds the state from scratch.

Opening index.html straight from disk breaks module and asset loading in most browsers, so work through the dev server from the first line.

A few things reliably go wrong on a first build. A blurry canvas usually means it was sized in CSS pixels, and MDN’s fix is to size it in device pixels (CSS size times devicePixelRatio) and set the CSS size separately. Double speed after a restart means a second requestAnimationFrame chain started on reset and now runs alongside the first. And logic inside draw code makes pause and replay impossible, because the state changes every time something paints.

For phones, add the viewport meta tag and size the canvas to the visible area. Without it, mobile browsers scale the page down and touch targets shrink.

How Do You Handle Keyboard, Mouse, Touch and Controller Input?

Keyboard events cover keys, Pointer Events cover mouse, pen and touch, and the Gamepad API deals with controllers. Merge them into one input state that the update step reads.

Each device has its own browser API, and each one has a trap the first build rarely hits.

For the keyboard, read event.code and not event.key, so the movement cluster stays under the left hand on AZERTY and other layouts. Clear every held key on window blur, or a key released in another window stays stuck.

Pointer coordinates arrive in CSS pixels, so convert them to canvas pixels before any hit test, because a scaled canvas shifts every click. Call setPointerCapture on drags so they keep firing when the pointer leaves the canvas.

Gamepads are the odd one out. The API fires events on connect and disconnect, but button and axis state is polled, so call navigator.getGamepads() inside the update step on every frame.

Touch needs one line of CSS. By default the browser handles panning and pinching itself, and a page using Pointer Events receives pointercancel as soon as the browser takes over a gesture, per MDN’s touch-action reference.

Set touch-action to none on the canvas element only. MDN flags the cost: it blocks pinch zoom, which hurts players with low vision, so keep it off the rest of the page.

Put a virtual stick under the left thumb and action buttons under the right, and draw hit areas larger than the graphics.

How Do You Manage Assets and Audio?

Pack sprites into atlases and load everything before the first frame. Audio needs a tap or click before it plays at all.

Sprites and tilemaps

A sprite sheet holds the frames of one animation, while a texture atlas packs many unrelated images into one file and records their positions in a JSON map.

The payoff is fewer draw calls, not a smaller download. PixiJS’s documentation says a sprite sheet moves roughly the same bytes as separate images, and that sprites sharing one texture are more likely to batch into a single draw call.

GPU memory follows pixel dimensions at 4 bytes per pixel for RGBA, so a lean PNG on disk can still cost plenty of VRAM.

Tiled exports levels as JSON with layers, tilesets and tile dimensions (Tiled documentation), and Phaser loads that JSON directly.

  • Load every sprite sheet, tilemap and sound before the first frame
  • Show a progress bar, so a slow connection reads as loading and not as a frozen page
  • Unload textures when a level ends

Game audio

The Chromium project originally scheduled enforcement of its autoplay policy for the Web Audio API for Chrome 70, in October 2018. Chrome’s current autoplay guide dates Web Audio coverage from Chrome 71.

MDN’s best-practices guide describes the same rule as a decision by browser vendors. An audio context created outside a user gesture starts suspended and plays nothing until the page resumes it.

The fix is to create or resume the AudioContext inside the first click or tap handler. A title screen’s Play button supplies that gesture for free.

To detect the problem, create a context and read its state. Running means audio is allowed and suspended means blocked, according to the Chromium documentation.

Howler.js handles the plumbing. It defaults to Web Audio, falls back to HTML5 Audio, plays MP3, OGG, WAV, AAC and WEBM among other formats, and supports audio sprites, which pack several sounds into one file the way an atlas packs images.

How Do You Save Game Progress in the Browser?

localStorage covers settings and small progress records. IndexedDB takes over when saves get large or structured.

localStorage holds string-only key/value pairs, per MDN, which suits volume settings, a high score or a level number.

IndexedDB is a transactional, asynchronous database that stores any object the structured clone algorithm supports, files and blobs included. Reach for it when the save holds full world state or replay data.

MDN’s storage quota page currently states that Web Storage is limited to 10 MiB in total on all browsers, with up to 5 MiB of localStorage and 5 MiB of sessionStorage per origin.

Plan around 5 MiB for localStorage. A write past the limit throws a QuotaExceededError, so wrap writes in try/catch.

Quotas apply to a whole origin, and browsers can delete an origin’s data when the device runs low on storage, according to MDN. Calling navigator.storage.persist() asks for the persistent tier, which browsers evict only when the player clears the data. Firefox also raises the quota for persistent origins, while Chromium-based browsers apply the same ceiling in both modes.

Some failures are worth handling up front.

  • Poki’s requirements say Incognito Mode restricts localStorage, so every call sits inside try/catch.
  • If the player clears site data, the save vanishes. Offer an export code or a cloud save.
  • Older Safari versions prompt the player for permission once an origin reaches 1 GiB, while macOS 14 and iOS 17 and later allow roughly 60% of disk per origin in browser apps (MDN).

Portals add a twist. CrazyGames’ full integration links progress to the player’s CrazyGames account, and Poki offers an Arbitrary User Data Store for backend saves.

On a portal, browser storage is a cache. The account is the source of truth.

When Does HTML Game Development Fall Short?

It falls short when a game needs more memory, startup speed or sustained CPU than a browser tab allows.

Mobile memory hits first. WebKit bug reports record the warning “Total canvas memory use exceeds the maximum limit” at 256 MB on an iPad and 288 MB on an iPhone X under iOS 12, and at 384 MB on iOS 14 and 15 (WebKit Bugzilla).

The limit changes with device and iOS version. Third-party iOS browsers have historically run on WebKit, so switching browsers does not lift it (MDN).

The arithmetic is short. One 4096 by 4096 canvas at 4 bytes per pixel is 64 MiB, so six of them reach the 384 MB ceiling.

  • Reuse canvases. Create them once and shrink retired ones to 1 by 1 pixel instead of dropping references.
  • Lower the backing resolution. MDN recommends a smaller canvas scaled up with CSS over a larger canvas scaled down.
  • Expect a sharpness cost, since a canvas sized in device pixels on a 3x phone display holds 9 times the pixels of a CSS-pixel canvas.

CPU-bound logic is the next wall. Pathfinding and large physics steps compete with drawing on one main thread.

Move the hot loop to a Web Worker or to WebAssembly, which MDN describes as a low-level, assembly-like language that runs alongside JavaScript with near-native performance. Pool bullets, particles and enemies so garbage collection has less to pause for.

Download size matters because every HTML build is a download before it is a game. itch.io’s documentation advises a downloadable version once a build outgrows its limits, because very large HTML5 games load and run worse than the equivalent download.

Low-end hardware is easy to forget. CrazyGames’ technical requirements note that a significant segment of its audience plays on Chromebooks, so test on one.

Treat cross-browser compatibility as a release gate. Run the build in Chrome, Firefox and Safari, on desktop and on a phone, before submitting anywhere.

A downloadable build makes more sense if the game can’t fit a portal’s size budget even after compression, if it needs sustained multi-threaded CPU work, or if your audience skews toward older phones with tight memory ceilings.

How Do You Publish and Monetize an HTML Game?

itch.io gives you control, Poki and CrazyGames bring traffic and ad revenue, and your own static host leaves every term in your hands.

ChannelBuild rulesRevenue
itch.ioZIP with index.html, 1,000 files, 500 MB extracted, 200 MB per fileDonations on HTML5 pages
Poki16:9 canvas, external requests blocked by default, no ads other than the Poki SDK’s100% of direct traffic, 50/50 on Poki traffic
CrazyGames50 MB initial download, 250 MB total, 1,500 filesAd share through its SDK
Self-hostedAny static HTTPS hostYour own ads or payments

Portal rules that break builds

On itch.io, file names are case sensitive on the server, while macOS and Windows ignore case locally. A game that runs on your machine can return a 403 after upload. Click to play is on by default, and turning it off to auto-start can leave audio muted on some browsers.

Poki blocks all external requests by default, so Google Fonts, CDN libraries and externally hosted assets must be bundled into the build. Splash screens and other ads come out too, and the game must run on mobile and tablet.

CrazyGames splits launches in two. Basic Launch puts a game live without customization, with the SDK optional and monetization unavailable. Full Launch requires the SDK and every integration requirement.

Read each portal’s terms before building. The rules differ enough that a build made for one often needs rework for the next.

Desktop and mobile wrappers

Electron packages a web game as a desktop app.

Its documentation says it embeds Chromium and Node.js into the binary, so one JavaScript codebase builds for Windows, macOS and Linux. The download grows to match, because every build carries its own Chromium.

A progressive web app manifest and service worker add an install prompt and offline play to a self-hosted build, with no store review. Mobile app stores need a native wrapper and their own review, which makes them a separate project from the browser build.

Revenue paths

  • Ads served through a portal SDK
  • Donations on an itch.io HTML5 page, which takes payments only as donations
  • In-game purchases for selected CrazyGames titles, processed through Xsolla
  • Paid sales through a downloadable build on itch.io
  • A one-time flat license fee, which Poki offers for some games in place of revenue share and marketing support

Poki prefers Web Exclusive deals, where the game appears only on Poki across the open web, in exchange for additional investment (Poki deal terms). Decide on exclusivity before signing, because it removes the multi-portal option.

HTML Game Development FAQ

Do you need JavaScript to make an HTML game?

Yes, for anything beyond a trivial click toy. HTML and CSS cannot run a game loop, read keyboard state or test collisions, so JavaScript carries the logic.

No-code editors such as Construct 3 are the exception, because they build that logic from visual events.

Can HTML games run offline?

Yes, once a service worker caches the build files. IndexedDB keeps saves on the device.

Online features such as portal ads, cloud saves and multiplayer still need a network.

How do you build a multiplayer browser game?

Connect players through WebSocket to a game server, or through WebRTC data channels for peer-to-peer play. Poki also offers a Networking Library called Netlib for multiplayer games on its platform.

Poki blocks external requests by default but exempts multiplayer games that supply a privacy statement.

Is HTML game development a good start for beginners?

Yes. A first game needs only a browser, a code editor and Node.js for the Vite dev server, and one canvas element covers the drawing.

Start with vanilla JavaScript and a small arcade game before touching a framework.

Did HTML5 replace Flash for browser games?

Largely, yes. The canvas element, WebGL and the Web Audio API now cover natively what the Flash plugin did, with no install.

Poki’s developer FAQ still offers a tool that converts Flash games to HTML5, with a contact address for publishing them on its platform.

When to Move from Vanilla JavaScript to an Engine

Project size is a poor reason to adopt an engine. A measured frame budget is a good one: 16.7 ms per frame at 60 Hz, and only 6.9 ms at 144 Hz.

A one-screen brick-breaker is the smallest game worth building first, because it exercises the loop, input, rectangle collisions and a restart state. Packed into a single HTML file, it uploads to itch.io without zipping.

Build it in vanilla JavaScript, time every frame on the weakest target device, and replace only the system that exceeds the budget. The order matters because an engine adds weight and upgrade work, so profiling should name the system that needs replacing.

What an engine buys is scene management and batching. What it costs is control, since the project is now tied to the engine’s release cycle.

This advice reflects documentation retrieved in October 2026 and changes once WebGPU covers every major platform.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)