DEV3 Technologies home

Sandboxed iframe code playground

A React HTML, CSS and JavaScript playground that runs user code in a sandboxed iframe with a debounced srcDoc update.

React / Next.jsUtilitiesreactiframesandboxplayground

Code

'use client';
import { useEffect, useState } from 'react';

const starter = {
  html: '<button id="go">Click me</button>\n<p id="out"></p>',
  css: 'body { font-family: system-ui; padding: 1rem }',
  js: "document.getElementById('go').onclick = () => {\n  document.getElementById('out').textContent = 'Hello from the sandbox';\n};",
};
type Code = typeof starter;

// CSP keeps the frame offline; the sandbox gives it an opaque origin.
function buildDoc({ html, css, js }: Code) {
  const safeJs = js.replace(/<\/script/gi, '<\\/script'); // cannot close the tag early
  return `<!doctype html><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:">
<style>${css}</style>${html}<script>${safeJs}</script>`;
}

export function Playground() {
  const [code, setCode] = useState<Code>(starter);
  const [doc, setDoc] = useState(() => buildDoc(starter));

  // Debounce so typing does not reload the frame on every key press.
  useEffect(() => {
    const t = setTimeout(() => setDoc(buildDoc(code)), 400);
    return () => clearTimeout(t);
  }, [code]);

  return (
    <div className="grid gap-3 md:grid-cols-2">
      <div className="space-y-2">
        {(['html', 'css', 'js'] as const).map((lang) => (
          <label key={lang} className="block text-xs font-semibold uppercase text-slate-500">
            {lang}
            <textarea
              spellCheck={false}
              value={code[lang]}
              onChange={(e) => setCode((c) => ({ ...c, [lang]: e.target.value }))}
              className="mt-1 h-28 w-full rounded border p-2 font-mono text-sm normal-case text-slate-900"
            />
          </label>
        ))}
      </div>
      {/* allow-scripts WITHOUT allow-same-origin: no cookies, storage or parent access */}
      <iframe title="Result" sandbox="allow-scripts" srcDoc={doc} className="h-full min-h-80 w-full rounded border bg-white" />
    </div>
  );
}

How it works

What it solves

Code playgrounds let people try snippets live, but running arbitrary HTML and JavaScript inside your own page is a serious security risk. This component executes the code in an isolated frame so it cannot read your cookies, storage or DOM.

How it works

The three editors hold HTML, CSS and JavaScript in state. A combined document string is built with buildDoc: styles in the head, markup in the body and the script at the end. The string is passed to the iframe through srcDoc, which loads it as a separate document.

Safety comes from the sandbox="allow-scripts" attribute without allow-same-origin. Scripts run, but the frame gets an opaque origin, so it cannot touch document.cookie, localStorage or the parent window, and cannot navigate the top page. A Content Security Policy meta tag inside the generated document adds a second layer by blocking network access to unknown hosts, keeping the default demo offline.

Typing triggers a rebuild, but reloading an iframe on every keystroke is wasteful and interrupts running animations. A 400 ms debounce, implemented with an effect and setTimeout, delays the update until the user pauses. Closing </script> text inside user JavaScript is escaped so it cannot break out of the script tag.

Adapting it

  • Capture console.log by posting messages from the frame to the parent.
  • Add a reset button restoring the starter code.
  • Use a resizable split with CSS grid.

Pitfalls

  • Never add allow-same-origin together with allow-scripts for untrusted code; that defeats the sandbox.
  • srcDoc size is limited by the browser; keep snippets small.

How to use it

  1. Save as components/Playground.tsx (client component).
  2. No dependencies.
  3. Render <Playground />; edit the starter constants for default code.
  4. To log output, add window.addEventListener('message', ...) in the parent and postMessage from the frame.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets