Developer rebuilds Doom using CSS and HTML, turning divs into a full 3D game engine

Skye Jacobs

Posts: 2,029   +59
Staff
In a nutshell: Modern CSS has been pushed into some unusual places over the past few years, but few experiments stretch it as far as a fully playable version of Doom rendered entirely with HTML elements and style rules. In a recent project, Dutch developer Niels Leenheer rebuilt the classic shooter's visuals so that every wall, floor, barrel, and imp is a div transformed in 3D space, with JavaScript handling the game loop and CSS taking over all of the rendering.

Leenheer is best known for creating HTML5test.com, the WhichBrowser user-agent parser. He began exploring a CSS-based Doom renderer after completing an earlier experiment that ran the game on an oscilloscope, reusing code to extract map data from the original WAD file.

His latest experiment builds directly on that work. The extracted data is compiled into a static scene composed of thousands of div elements, each annotated with raw Doom coordinates. The browser's layout and compositing systems then render these as positioned 3D objects using CSS transforms and math functions such as hypot () and atan2 ().

Instead of having JavaScript compute the full 3D geometry, the script passes through the original coordinate pairs along with floor and ceiling heights as custom properties, leaving the CSS engine to perform the trigonometry for wall width and rotation.

The separation is deliberate: JavaScript runs a game loop derived from the open-sourced C code for Doom, while a thin rendering layer exposes only new coordinates, state attributes, and a handful of custom properties to CSS. Attempts to keep game state and logic entirely in CSS were abandoned as impractical, but the rendering path remains almost entirely on the styling side.

Much of the project comes down to reconciling Doom's coordinate system and rendering tricks with how browsers handle 3D. Floors are modeled as div elements rotated 90 degrees around the X axis, then clipped into arbitrary sector polygons using clip-path with polygon () or the newer shape () syntax to support complex outlines and even-odd fill rules.

Texture tiling across sectors is kept seamless by anchoring background positions in "world" coordinates. For example, if an element is offset by 200px horizontally and 400px vertically, the background-position is set to -200px -400px so patterns line up as if they were painted across a continuous plane.

Movement works by shifting the world rather than using a camera, which doesn't exist in CSS. JavaScript tracks the player's position and angle in four custom properties, and CSS inverts those values to move the scene in the opposite direction, compensates for perspective with an extra translate, and leaves the rest to the browser.

Sprite handling for enemies and projectiles relies on billboard techniques: CSS rotates each sprite to face the viewer, mirrors frames with scaleX when needed, and advances spritesheets with step () animations, while JavaScript updates state attributes.

Lighting, doors, lifts, and projectiles are all treated as stateful style problems. Sector light levels are stored as a cascaded custom property used with filter: brightness (), so everything inside a dark sector is dimmed without adjusting each element individually. Opening a door moves a container upward via a CSS transition on a custom property; the game loop only toggles a data attribute, leaving the animation to the browser. Projectiles are created with start and end coordinates and a duration; CSS animates them from point A to B using translate (), while a separate rotate () property keeps them facing the player.

One of the more unusual applications of modern layout features is making the game responsive. The DOM-based HUD is reconstructed from the original fixed-width status bar into separate elements for ammo, health, face, armor, and keys, laid out with Flexbox so they wrap naturally on narrow screens.

A spectator mode adds another layer of CSS-driven camera logic. A follow view positions the camera behind and above the player using CSS calc () together with sin () and cos () on the player's angle to derive offsets, and uses separate translate () and rotate () properties so transitions between first-person and follow views interpolate smoothly.

Performance and correctness issues exposed some limits of this approach. Large maps with thousands of 3D-transformed elements can push browser compositors into stutter – or even crashes – on mobile Safari, so Leenheer added culling to hide geometry outside the player's field of view.

A straightforward JavaScript implementation walks through the elements and toggles a hidden attribute based on distance and angle. He also experimented with a pure-CSS variant that computes a visibility flag and then uses a "type grinding" trick: a paused animation whose delay is set via a custom property to land on keyframes that either show or hide the element.

Some parts of Doom's original renderer simply do not translate cleanly. The game draws sky textures in effectively 2D on "walls" that can sit in front of real map geometry, whereas the CSS version must project a true 3D scene with the sky drawn behind it. This causes some surfaces that Doom hides behind the sky to become visible; correcting this required an additional culling step to hide objects positioned behind sky walls from the player's viewpoint.

Leenheer is clear that the project is not a replacement for a WebGL or WebGPU renderer and that performance remains constrained, but he argues that this is not the point. "This is about pushing the boundaries of what CSS can do," he writes, pointing to trig functions, @property animations, clip-path, SVG filters, and anchor positioning as "production-ready CSS features being used in ways their spec authors probably never imagined."

The result is a playable demonstration showing that, with enough div elements and a generous use of modern CSS, Doom's world can be reconstructed in a browser without relying on a graphics API at all.

CSS is the latest in a growing list of unusually low-tech devices and other objects that people have tricked into running Doom. Others that can run the classic FPS include:

Permalink to story:

 
I'm so sick of these programming bragging endeavors that are completely useless in a real job interview (if you ever get there).

Nobody ever asked me anything fun I coded. I only get: Sorry, but you are a fossil who manually manages his own databases. We use AWS so if Amazon is down, our whole company if f*ck*d.
 
I'm tired of hearing about all the sports that got Doom to run on a sucha sucha, it is not news worthy
 
Title: only CSS and HTML
Article: JavaScript runs a game loop
You should be ashamed. It's well known that CSS3 is Turing complete. I think I actually believed the title.
 
Title: only CSS and HTML
Article: JavaScript runs a game loop
You should be ashamed. It's well known that CSS3 is Turing complete. I think I actually believed the title.
Hrm. Perhaps I missed something somewhere but I think this claim confuses “can express a transition rule” with “can iterate that rule over evolving state.”

Even including the browser, the system fundamentally lacks a state transition mechanism:

- The DOM is the state
- CSS computes a rendering from that state
- The result is not written back as a new state

So you never get:
state_n -> state_(n+1)

Without that, you don’t have iterative computation—just repeated evaluation of a fixed function.

That also implies:
- No unbounded memory (DOM is fixed)
- No general state persistence
- No feedback loop

So the reduction to something like Rule 110 doesn’t go through.

Conclusion: HTML + CSS + browser are not Turing complete.

That’s not to suggest you can’t do some pretty interesting and complex stuff with the basics, just that the claim itself isn’t wholly accurate.
 
I'm so sick of these programming bragging endeavors that are completely useless in a real job interview (if you ever get there).

Nobody ever asked me anything fun I coded. I only get: Sorry, but you are a fossil who manually manages his own databases. We use AWS so if Amazon is down, our whole company if f*ck*d.
you are confusing work with hobby. people are not allowed top use their work knowledge/skills to do things they like?

I've done plenty of wacky stuff with programming over the years, none of which I've used to get a job. but it did help me learn new things or brush up on things I rarely use but still needed to know about. and later on it did help me apply them in many work related situations. css+js animations have especially been very useful.
 
Back