npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

code-playpad

v0.2.1

Published

Embeddable browser code playground: a CodeMirror editor that runs Python, JavaScript and TypeScript in the browser. No backend. One shared worker per page, blocking input(), multi-file projects, lazy per-language loading.

Readme

code-playpad

npm license

Runnable code in any web page. A CodeMirror editor that executes what it shows — Python, JavaScript and TypeScript — entirely in the reader's browser. No backend, no iframe, no sign-up.

Live demo →

<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]"></script>

<code-playpad height="auto">
  print("hello from a static page")
</code-playpad>

Contents — Languages · Install · Attributes · Sizing · Several files · Input · Controlling it from JavaScript · Events and grading · Saving work · Light and dark · Styling · Framework recipes · Production · Worth knowing

Languages

| language | Runtime | Downloaded at runtime | | --- | --- | --- | | python (default) | Python 3.14 via Pyodide — full standard library, input(), real tracebacks, numpy on demand | ~11 MB, once, cached | | javascript | The browser's own engine, in a Worker | nothing | | typescript | Same engine, types stripped before running | ~50 KB, once, cached |

<code-playpad height="auto">print("python is the default")</code-playpad>

<code-playpad language="javascript" height="auto">
  const doubled = [1, 2, 3].map((n) => n * 2);
  console.log(doubled);
  doubled.reduce((a, b) => a + b, 0)
</code-playpad>

<code-playpad language="typescript" height="auto">
  type Point = { x: number; y: number };
  const dist = (p: Point): number => Math.hypot(p.x, p.y);
  dist({ x: 3, y: 4 })
</code-playpad>

A trailing expression prints its value, like a REPL. Each language loads only on pages that use it: a Python-only page never downloads a byte of the JavaScript runtime, and a JavaScript-only page never touches Pyodide's 11 MB.

TypeScript here erases types, it does not check them — a type error will not stop your code running. This is a playground, not tsc.

Install

From a CDN — nothing to install

<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]"></script>

This is the standalone build: CodeMirror is bundled in, so a static blog post or a Markdown-rendered page needs only that one tag. Pin the version — an unpinned URL follows latest and will change under your readers.

From npm, with a bundler

npm install code-playpad @codemirror/state @codemirror/view @codemirror/commands \
  @codemirror/language @codemirror/autocomplete @codemirror/theme-one-dark

CodeMirror is a peer dependency so you never end up with two copies of @codemirror/state. Language grammars are optional — install only what you use (@codemirror/lang-python, @codemirror/lang-javascript, @codemirror/lang-json); a missing one degrades to plain text rather than throwing.

import 'code-playpad/element';        // registers <code-playpad>

If your bundler is configured to drop side-effect-only imports, register explicitly instead:

import { defineCodePlayPad } from 'code-playpad/element';
defineCodePlayPad();

Entry points

| Import | You get | | --- | --- | | code-playpad/element | registers <code-playpad>; smallest thing that works | | code-playpad | the same, plus every helper below | | code-playpad/react | the <CodePlayPad> React component | | code-playpad/standalone | CodeMirror bundled in — what the CDN serves |

Importing on a server is safe: without a DOM the element degrades to a no-op, so Next.js, Astro and SvelteKit need no dynamic import or client:only guard.

Attributes

| | | | --- | --- | | language | python (default), javascript, typescript | | height | number (px), any CSS length, or auto | | min-height / max-height | bounds for auto | | theme | light, dark, or auto (default — follows the OS) | | readonly | editor cannot be typed into; Run still works | | autorun | runs once, on its own, when scrolled into view | | stdin | shows the standard-input box up front | | persist | key under which edits are saved (see Saving work) | | entry | which file Run executes, for multi-file widgets |

<!-- a worked example nobody should edit -->
<code-playpad height="auto" readonly>
  print("read this, don't change it")
</code-playpad>

<!-- output visible before the reader does anything -->
<code-playpad height="auto" autorun>
  print("this already ran")
</code-playpad>

<!-- always dark, regardless of the reader's OS setting -->
<code-playpad height="auto" theme="dark">
  print("always dark")
</code-playpad>

readonly blocks typing but not execution, and the code can still be replaced from JavaScript — useful for "show the answer" buttons.

Note it is language, not lang: lang is a standard HTML attribute for the human language of an element's text, and putting python there confuses screen readers.

Sizing

height takes a number (pixels), any CSS length, or auto to fit the snippet:

<code-playpad height="auto">print("no dead space, no hidden lines")</code-playpad>

auto is usually what you want. A fixed height has to be guessed per snippet, and no guess is right at every viewport width — lines wrap, so a 90-character line is one row on a desktop and three on a phone.

Two bounds go with it:

<!-- a long listing that should not push the page off screen -->
<code-playpad height="auto" max-height="400">…</code-playpad>

<!-- an answer box: one line of content, but room to type -->
<code-playpad height="auto" min-height="170"># Your code here</code-playpad>

max-height scrolls rather than clips. min-height grows the editor itself, so the empty space below a short snippet is still part of the editor and clicking it puts the cursor there. All three take a bare number as pixels, or any CSS length (40vh, 30em).

Several files

A widget can hold a small project. Relative imports, packages and data files all work, and tracebacks name the real file and line.

<code-playpad entry="main.py" height="auto" max-height="420">
  <playpad-file name="main.py">
    from geometry import Circle
    print(Circle(1).area())
  </playpad-file>

  <playpad-file name="geometry/__init__.py">
    from .shapes import Circle
  </playpad-file>

  <playpad-file name="geometry/shapes.py">
    from math import pi

    class Circle:
        def __init__(self, radius): self.radius = radius
        def area(self): return pi * self.radius ** 2
  </playpad-file>
</code-playpad>

JavaScript and TypeScript work the same way, including .json imported as data:

<code-playpad language="javascript" entry="main.js" height="auto">
  <playpad-file name="main.js">
    import { total } from "./cart.js";
    import items from "./items.json";

    console.log(total(items));
  </playpad-file>

  <playpad-file name="cart.js">
    export const total = (items) =>
      items.reduce((sum, item) => sum + item.price, 0);
  </playpad-file>

  <playpad-file name="items.json">
    [{ "price": 12 }, { "price": 30 }]
  </playpad-file>
</code-playpad>

A tab bar appears once there is more than one file, with ▸ marking the entry point, and each file keeps its own undo history. Editing a module takes effect on the next run — the import cache is cleared between runs.

Highlighting follows the file, not the runtime: a .json beside your JavaScript is highlighted as JSON, a .csv beside your Python is plain text.

Input

input() blocks for real. The prompt appears, a caret waits at the end of the output, and the program continues when a line is sent — echoed the way a terminal echoes typing:

name? Ada
age? 36
hello Ada, next year you turn 37

Enter sends a line; EOF (or Ctrl-D) ends input, which is how you finish a while True: loop.

In JavaScript and TypeScript, input() is a global returning a promise, so await works at the top level:

<code-playpad language="javascript" height="auto">
  const name = await input("your name? ");
  console.log(`hello ${name}`);
</code-playpad>

To script the answers instead of typing them — useful for a worked example — pre-fill the box. Those lines are used before the reader is asked:

<code-playpad height="auto" stdin>
  name = input("name? ")
  print("hi", name)
</code-playpad>
document.querySelector('code-playpad').stdin = 'Ada\n36';

Echoed input is display-only: stdout stays exactly what the program printed, so graders are unaffected.

Controlling it from JavaScript

const pad = document.querySelector('code-playpad');

pad.code = 'print("set from outside")';   // replace the visible file
const source = pad.getCode();             // read it back
const result = await pad.run();           // { stdout, stderr?, value?, durationMs }
pad.reset();                              // back to the snippet the page shipped
pad.stop();                               // kill a runaway program
await pad.prewarm();                      // boot the runtime before the reader runs anything

Multi-file widgets add:

pad.files = {                             // replace the whole project
  'main.py': 'from util import shout\nprint(shout("hi"))',
  'util.py': 'def shout(s): return s.upper() + "!"'
};
pad.entry = 'main.py';                    // which file Run executes
pad.openFile('util.py');                  // bring a tab on screen
const everything = pad.getFiles();        // { name: contents }

prewarm() is worth calling when you know the reader is about to run something — on a lesson page, for instance — so the ~11 MB Python download starts before they press Run rather than after.

Events and grading

Four events bubble and cross the shadow boundary, so answer-checking lives outside the widget rather than inside it:

| event | event.detail | | --- | --- | | py-ready | { language, label } — runtime booted | | py-run | { code, entry, files, language } — a run started | | py-output | { stdout, stderr?, value?, table?, durationMs, language } | | py-error | { message, hint?, language } |

A complete exercise check:

<code-playpad id="ex1" height="auto" min-height="150" persist="ex1">
  # Print the numbers 1 to 5, one per line.
</code-playpad>
<p id="ex1-result"></p>
const expected = '1\n2\n3\n4\n5\n';

document.addEventListener('py-output', (event) => {
  if (event.target.id !== 'ex1') return;

  const el = document.getElementById('ex1-result');
  if (event.detail.stderr) {
    el.textContent = 'Your code raised an error — read the message above.';
  } else if (event.detail.stdout === expected) {
    el.textContent = '✅ Correct';
  } else {
    el.textContent = `❌ Expected ${JSON.stringify(expected)}, got ${JSON.stringify(event.detail.stdout)}`;
  }
});

py-error also carries a hint when the runtime can explain the failure in plain language — for example when a program calls input() more times than the standard-input box has lines.

Saving work

persist stores the whole file map in IndexedDB, so a reader's edits survive navigation and a full reload:

<code-playpad height="auto" persist="lesson-3-exercise-1">
  # Your code here
</code-playpad>

The page's file list stays authoritative: edits come back, but files you add or remove later follow the page, not the old snapshot.

For a course you usually also want completion state, which the package exports:

import { setProgress, getProgress, allProgress, loadSnippet, clearSnippet } from 'code-playpad';

// mark a lesson done when its exercise passes
document.addEventListener('py-output', async (event) => {
  if (event.detail.stdout === expected) {
    await setProgress('lesson-3', { completed: true, score: 100 });
  }
});

await getProgress('lesson-3');   // { id, completed, score, updatedAt }
await allProgress();             // every record, for a progress bar
await loadSnippet('lesson-3-exercise-1');   // what the reader last wrote
await clearSnippet('lesson-3-exercise-1');  // a "start over" button

All of it is per-browser: nothing leaves the reader's machine.

Light and dark

theme takes light, dark, or auto (the default).

<code-playpad theme="dark" height="auto">print("always dark")</code-playpad>
<code-playpad theme="light" height="auto">print("always light")</code-playpad>
<code-playpad height="auto">print("follows the page, then the OS")</code-playpad>

auto follows your site first, the operating system second. If your page has its own light/dark switch, the widget follows it as long as the switch sets color-scheme on the root element — which is how most themes are written:

:root { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

Flip data-theme (or a class, or an inline style) and every widget on the page re-themes itself. Nothing else to wire up. With no color-scheme on the page, auto falls back to the reader's OS setting via prefers-color-scheme.

If your theme switch does not set color-scheme, drive the widgets directly:

function setTheme(mode) {            // mode: 'light' | 'dark'
  document.documentElement.dataset.theme = mode;
  for (const pad of document.querySelectorAll('code-playpad')) {
    pad.setAttribute('theme', mode);
  }
}

An explicit theme attribute always wins over the page, so a deliberately dark snippet stays dark on a light page.

Styling

The widget lives in a shadow root, so page CSS cannot leak in — and its own styles cannot leak out. Two supported ways to restyle it.

Custom properties inherit through the shadow boundary:

code-playpad {
  --cp-accent: #b5179e;     /* buttons, focus, the caret */
  --cp-radius: 14px;
  --cp-font-mono: "JetBrains Mono", monospace;
}

| | | | --- | --- | | colours | --cp-bg, --cp-fg, --cp-muted, --cp-border, --cp-surface | | accents | --cp-accent, --cp-accent-fg, --cp-danger, --cp-ok | | shape and type | --cp-radius, --cp-font-mono, --cp-font-ui |

Parts expose the pieces worth targeting:

code-playpad::part(run-button) { border-radius: 999px; }
code-playpad::part(stop-button) { font-weight: 700; }
code-playpad::part(output) { background: #fff7fb; }
code-playpad::part(tabs) { border-bottom-width: 2px; }

Both work from an ordinary stylesheet, no ::shadow hacks. For dark mode, the theme attribute already follows the reader's OS by default.

Framework recipes

Astro — put the import in a page or layout script; the element does the rest.

---
// src/layouts/Lesson.astro
---
<script>
  import 'code-playpad/element';
</script>

<code-playpad height="auto" persist={Astro.props.id}>
  {Astro.props.code}
</code-playpad>

React — the wrapper assigns props through a ref, so React 18 behaves like 19.

import { useState } from 'react';
import { CodePlayPad } from 'code-playpad/react';

export function Exercise() {
  const [passed, setPassed] = useState(false);

  return (
    <>
      <CodePlayPad
        language="python"
        code={'# Your code here'}
        height="auto"
        minHeight={150}
        persist="exercise-1"
        onOutput={(r) => setPassed(r.stdout.trim() === '42')}
      />
      {passed && <p>✅ Correct</p>}
    </>
  );
}

Props: code, files, entry, language, height, stdin, theme, readOnly, autoRun, persist, className, style, and the callbacks onReady, onRun, onOutput, onError.

Next.js — the element is SSR-safe, so a plain import works in a client component:

'use client';
import 'code-playpad/element';

export default function Lesson() {
  return <code-playpad height="auto">{'print("hi")'}</code-playpad>;
}

Vue — tell the compiler the tag is a custom element, then use it directly:

// vite.config.js
export default {
  plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === 'code-playpad' } } })]
};

Plain HTML / Markdown — the CDN tag plus the element. Because the code lives in the element's text content, the snippet is still readable if JavaScript never loads.

Production

Serve Pyodide from your own origin so you control its cache headers, rather than depending on a public CDN:

import { configurePython, pyodideAssetUrls } from 'code-playpad';

configurePython({ indexURL: '/pyodide/' });   // before the first run
pyodideAssetUrls();                            // the files worth <link rel=preload>-ing

configureTypeScript({ sucraseURL }) does the same for the TypeScript transform. Give those files a long Cache-Control: one page's download then warms every other page on the site.

Worth knowing

  • One runtime per page. Every widget shares a single Worker per language, so ten editors cost one download, not ten. Runs are queued: Python is single-threaded, so a second widget waits for the first.
  • Runaway code cannot freeze the page. Code runs in a Worker; Stop terminates and reboots it. Runs queued behind the stopped one survive.
  • A program waiting on input() holds the shared runtime. Other widgets show Queued… until it finishes or is stopped.
  • Nothing boots on page load. An IntersectionObserver starts a runtime as a widget nears the viewport; touching the editor also wakes it. Use prewarm() to start earlier on purpose.
  • Python in the browser has limits: no threads, no multiprocessing, no sockets, no subprocess. requests works but goes through browser fetch, so CORS applies.
  • If CodeMirror fails to load, the widget falls back to a plain textarea that still runs code, rather than showing nothing.

Version history

No breaking changes so far: markup and API written against 0.1.3 still work on 0.2.1.

0.2.1

  • theme="auto" now follows the page before the operating system: if your site's light/dark switch sets color-scheme on the root, widgets re-theme with it. Previously a page could go dark while every widget stayed light.
  • minHeight and maxHeight props on the React wrapper — 0.2.0 added them as attributes but missed the wrapper.
  • README expanded into full usage documentation: entry points, an example per attribute, programmatic control, a complete grading example, the progress store, styling, and framework recipes.

0.2.0

  • height="auto" is a supported content-fitting mode. It previously worked by accident and could have broken silently.
  • New min-height and max-height attributes. max-height scrolls rather than clipping; min-height grows the editor itself, so the empty space below a short snippet is still clickable — what an exercise answer box needs.

0.1.4

  • Fixed: a <pre> or other wrapper on its own line leaked its indentation into the program, so two-line snippets failed with IndentationError while single-line ones looked fine.
  • Fixed: sideEffects named the re-export shims rather than the chunk that registers the element, so a bundler could legitimately drop it and <code-playpad> would never register in a production build.
  • No runtime dependencies (an analytics package used only by the demo had crept into dependencies).

0.1.3

  • Relicensed MIT. Earlier versions were published as UNLICENSED, which granted nobody the right to use them.

Versions 0.1.0 to 0.1.2 were withdrawn and are not installable.

Licence

MIT © 2026 Seung Hun Lee — see LICENSE.

The source is not published, but the package you install is MIT: use it, ship it, modify it. Bundled third-party components (CodeMirror 6, Lezer) remain under their own MIT licences, reproduced in full in dist/THIRD-PARTY-NOTICES.txt. Pyodide (MPL-2.0) is fetched from a CDN at runtime and is not redistributed by this package.