MDN Ditched React for Web Components and It Paid Off
Published on 13.04.2026
Under the Hood of MDN's New Frontend
TLDR: MDN replaced a heavily indebted React SPA with a custom architecture built on Lit web components and server-rendered HTML templates. The rewrite solved three problems at once: static content no longer ships redundant JavaScript, interactive pieces are now self-contained components, and the dev environment went from two minutes to two seconds.
This one is worth your time if you've ever shipped a React app and quietly wondered whether React was actually the right tool for the job. The MDN team walked into that exact question and came out the other side with something genuinely interesting.
The old MDN frontend was a Create React App that had been ejected, which is basically the engineering equivalent of opening a computer and hot-gluing extra parts inside. They had a complicated Webpack config, a mess of Sass mixed with CSS variables, CSS so entangled that changing one component broke unrelated ones, and the worst part: React was just a wrapper around static documentation content. They couldn't use React to work with the HTML generated by their build tool, so they fell back to dangerouslySetInnerHTML, and then used raw DOM APIs for interactivity inside that static content. Two different programming models for the same page. That's a maintenance nightmare.
The fix started with a small experiment: Lit and web components. Their first real use case was embedding Scrimba's interactive coding screencasts into MDN Curriculum pages. They built a custom element that deferred loading the iframe until a user clicked, used the native dialog element for fullscreen, and managed state through Lit's reactive properties. The component handles its own lifecycle, encapsulates its styles, and can be dropped directly into Markdown content as a plain HTML tag. That's the kind of thing that would have been genuinely painful with DOM APIs alone, and surprisingly it was simpler than the React equivalent would have been for this particular case.
From there they tackled interactive examples, the "Try it" widgets you see at the top of CSS and JavaScript reference pages. That was a bigger lift: multiple templates for different example types, a code editor built on CodeMirror, a console renderer, an output renderer, and a controller to wire them together. They ported the Playground logic piece by piece into web components, which Lit's React integration let them embed into the existing React app temporarily. No big-bang rewrite required. Once the pieces were in place they built an interactive-example custom element that authors can activate from Markdown with a macro, followed by fenced code blocks. Writers no longer need to touch four separate git repositories to add an example.
The server-side story is where things get architecturally ambitious. Instead of React Server Components (which would have required adopting a new framework anyway), the team built their own concept: server components using Lit's HTML template literal, rendered in Node. These run once at build time, produce static HTML, and can compose other server components or drop in web component tags. Client-side, they wrote a small loop that scans the DOM for any element whose tag starts with "mdn-" and lazily imports its JavaScript in parallel. Zero manual wiring. If the component is on the page, its code loads. If not, it doesn't. CSS is handled similarly: a tracking mechanism in the ServerComponent base class records which components actually rendered, and only those components' stylesheets get linked in the page head. The result is that users download exactly the CSS and JavaScript their current page needs, nothing more.