My coding playground

A world-class code playground with CodeMirror and esbuild

September 17, 2025

One of the most important elements in a developer blog or technical documentation is the code playground. Also known as a “sandbox”, this widget lets you edit a code snippet and instantly see the results. Here's the playground I use on this blog:

Recently, I started thinking about writing blog posts on TypeScript and UI development. That raised an important question: how should I showcase code examples? In my previous Neovim posts, I used the Astro code component, which provides syntax highlighting and great theming. But this time, I wanted to go further. Instead of just displaying static snippets, I wanted the code to run live and give readers the ability to experiment with it directly.

My requirements

  • 1. An intuitive code editor
  • 2. It must be interactive
  • 3. It needs to support TypeScript
  • 4. No third party service
  • 5. Capture and display console output

The right editor

First I had to find a good editor the user could interact with, I had two candidates in mind, the Monaco Editor and CodeMirror. First one does also power vscode and is definitly a greate editor but it comes with a much larger footprint and many features i dont't really need like a builtin lsp client. So I decided to go with CodeMirror for now.

Making it interactive

Now that i had a great editor, the next challenge was figuing out how to process the code so that the user can see changes live. I solved this using an iframe, which is essentially an HTML element that lets you embed another HTML document inside your page. By combining the JavaScript code, the HTML and the CSS into a single string and passing it to the iframe srcdoc attribute, the browser treats it as a fully self-contained document. This means the iframe has its own execution context, so the code inside it runs in isolation from the parent page. It also allows you to safely run arbitrary JavaScript and apply styles without affecting the rest of your site, all without linking to an external HTML file.

Transpiling TypeScript

Since I plan to write most of the snippets in TypeScript, I needed a way to transpile TypeScript into JavaScript directly in the browser. Luckily there is esbuild, esbuild is an extremly fast modern bundler written in go that also supports typescript and jsx out of the box. Esbuild is also shipped as a wasm (WebAssembly) binary that we can execute directly in the browser. One small caveat is that esbuild does not do any type checking and type declarations are parsed and ignored, but it does support TypeScript-only syntax extensions like enums and converts them to JavaScript. More details can be found here.

One other nice thing about esbuild is that it can bundle code. This means we can use esm export and import syntax to define modules in separate files for better structure. The only thing we need to do is define how esbuild should resolve and load referenced files. This can be achieved by defining a small custom plugin that resolved and loads files directly from memory, instead of the filesystem. With this setup we can now transpile and bundle all of our tyepscript files and utilize all typescript features.

Capturing console output

Now we are able to write TypeScript, bundle it and run it together with our HTML and CSS in the iframe. The last missing piece is capturing the console output. We achieve this by injecting a small snippet into the iframe script that monkey-patches the original console.log, console.error, and console.warn functions and then sends their content back to the parent application via postMessage.

const parent = window.parent;
  ['log','error','warn'].forEach(fn => {
    const orig = console[fn];
    console[fn] = (...args) => {
      orig(...args);
      parent.postMessage({ type: 'console', level: fn, sandboxId: '${sandboxId}', args }, '*');
    };
  });

Wrapping up

With these pieces in place, we now have a solid and flexible foundation. There are still thinks I’d like to improve in the future, adding React support, enabling the use of additional npm dependencies, and further refining the editor experience, but for now, this setup provides a strong starting point.

Share

Comments (0)