Interactive JS — Live Demo Channel¶
The biggest win over Photoshop: parameters the reader can move. Static images can't be explored; interactive demos let the reader develop intuition by playing with the underlying model.
Part of the muriel skill — see the top-level index for mission, universal rules, and channel map.
For histories in rows, previews beside filters or scrubbers, or response curves beside controls, consult the sparkline repertoire. Keep preview, inspection, and committed selection distinct; expand through the host's canonical state and provide keyboard/touch access to the same detail.
When to use¶
- Blog post explainers where the reader should move parameters, not just read
- Paper figures with sliders (CHI / ETTAC / CIKM submissions can include live demo URLs)
- Worked examples and teaching artifacts
- Audio-reactive visualizers
- Eye-tracking replays with timeline scrubber
- Anything where "watching it move" matters more than "seeing one frame"
For prepared imagery driven by live input, or choreography whose meaning needs design work, read motion craft. It separates input, playback, and media choices while preserving canonical state and Muriel's timing rules; ordinary procedural demos need no media-production pipeline.
Constraints (from ~/CLAUDE.md)¶
- ES6 modules direct. No bundlers, no build step.
- WebGL is the primary graphics stack. Canvas2D for non-shader 2D, D3 for data binding to DOM/SVG, Three.js when 3D scene management beats raw WebGL.
- Defensive numerics:
isFinite()guards, NaN traps, clamp before passing to GLSL. - Small focused functions over mega-files.
- Inline comments explain "why."
Default scaffold — single-file HTML¶
<!DOCTYPE html>
<html data-mg-theme="dark">
<head>
<meta charset="utf-8">
<title>Demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/marginalia@latest/marginalia.css">
<style>
:root { color-scheme: dark; }
body { max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
canvas { width: 100%; aspect-ratio: 16/9; display: block; background: #0a0a0f; }
.controls { display: flex; gap: 1rem; padding: 1rem 0; }
</style>
</head>
<body>
<main>
<h1>Title</h1>
<p>One-paragraph context.</p>
<canvas id="stage" width="1280" height="720"></canvas>
<div class="controls">
<label>Param <input type="range" id="p1" min="0" max="1" step="0.01" value="0.5"></label>
</div>
</main>
<script type="module">
'use strict';
const cvs = document.getElementById('stage');
const ctx = cvs.getContext('2d'); // or 'webgl2'
const p1 = document.getElementById('p1');
function frame() {
const v = parseFloat(p1.value);
if (!isFinite(v)) return requestAnimationFrame(frame);
// ... draw using v ...
requestAnimationFrame(frame);
}
frame();
</script>
</body>
</html>
One file. No build. Marginalia handles editorial chrome. Demo lives at <project>/demos/<name>.html and can be linked from a blog post or paper.
Library choices¶
| Need | Library | Why |
|---|---|---|
| Shaders / 3D / perf | WebGL2 raw + Three.js for scenes | Primary stack for shader-heavy work |
| Data binding to DOM/SVG | D3 v7 | Reactive scales, no virtual DOM |
| 2D sketches / teaching | p5.js | Lowest activation energy |
| Audio-reactive | Web Audio API + AnalyserNode | Native, zero deps |
| Eye-tracking replay | Custom Canvas2D + rAF | Fixation timing matters; no abstraction |
| Reactive UI controls | Vanilla <input type="range"> |
No framework needed for sliders |
| Rich typographic layout | @chenglou/pretext |
Non-DOM text measurement + multi-line layout + rich inline spans. See Typographic canvas with pretext. |
Permalink convention¶
Demos with shareable state should follow a PermalinkManager pattern: URL hash encodes parameters, parsed on load, rewritten on change. One utility per project, referenced from every demo — if the project already has one, read it first and don't reinvent it.
Zooming interfaces, semantic zooming, linked displays, and brushing¶
Use the multiscale coordination reference when scale changes what can be understood, or several views must answer one question together. It owns the behavioral contract; this channel owns the interactive surface. A conventional slider or isolated chart needs no extra coordination machinery.
For navigation through an information space, read zooming interfaces: useful camera bounds, landmarks, direct jumps, Fit/Back, automatic detail, and the Wunderkammer precedent. Semantic zoom determines how the representation changes; a ZUI also has to make the route into detail and back understandable and controllable.
For exploration through named attributes, read faceted exploration: explicit OR/AND scope, counts that retain other filters while omitting a facet's own restriction, local value search, active-value retention, and granular recovery. Local option search browses labels; global search and committed facets change membership.
| Mechanism | What changes | What stays identifiable |
|---|---|---|
| Semantic zoom | Representation: distribution → records → readable details | Entities, membership, chosen focus, and task meaning |
| Linked displays | Complementary projections respond to a shared query or selection | Stable entity keys and explicit relationships between datasets |
| Brushing | A gesture or accessible control specifies a predicate or set | Its scope, operation, selected population, and recovery path |
| Faceted exploration | Named attribute choices refine the working subset | Combination rules, count context, active restrictions, and recovery |
Build the contract before the marks¶
- Separate viewport, filters, committed selection, brush preview, hover, and pin. A pan does not silently filter; a hover does not overwrite a pin.
- Define one canonical owner in the host product. Each view emits semantic actions and derives its display from the accepted state. Do not install a second store just to use a chart library.
- Link through record IDs or documented relational keys. A geographic map, timeline, and table may use different coordinate systems. Comparable context strips should retain their common full domain; a zoomed detail view has its own explicitly labeled domain.
- Specify whether brushing highlights, filters, or navigates. Keep these actions distinguishable even when the same rectangular gesture can express all three. If combining brushes, expose replace/add/subtract/intersect and define how an inactive selection differs from an active empty one.
- Preserve aggregate membership. A bin containing two selected records out of twelve must not imply that all twelve are selected. Name count denominators and the population excluded by filters or the viewport.
- Choose semantic levels using available screen space and task needs. Preserve the world point under the zoom anchor, use hysteresis near representation boundaries, and retain an overview or route back. A larger font alone does not establish semantic zoom.
- Compose an in-progress brush against its starting selection. Commit once; cancel restores it. Store extents in data coordinates and recompute pixel geometry after resize. Batch previews at animation-frame cadence; invalidate stale async results before publishing them.
- On small charts, draw cursors and range bands as DOM overlays and move them
with CSS
leftandwidth. Do not rebuild EChartsmarkLineormarkAreaon every pointer move. - Provide non-drag, non-hover access to the same operations. Keep visible text at the contrast floor; use outlines, symbols, and counts for selection rather than fading essential text.
Working proof¶
Linked Atlas is a local, dependency-free reference with synthetic field observations: an overview, semantic detail, linked cohort summaries, record table, and pin/preview inspector. Its pure model and browser checks exercise selection preservation, cancellation, coordinate conversion, semantic levels, keyboard access, and mobile layout.
Borrow the contract and the smallest relevant implementation seam. Do not copy its dataset, threshold constants, palette, or whole state object into an existing product. Use library adapters for the chosen renderer and the evaluation battery for transfer to other domains and honest task-level proof.
Marginalia integration¶
Wrap demos in marginalia callouts for editorial context:
<aside class="mg-callout" data-type="tip">
<h3>Try it</h3>
<iframe src="demos/foveation.html" loading="lazy" style="width:100%;border:0;aspect-ratio:16/9"></iframe>
<p>Drag the slider to see how cortical magnification compresses peripheral vision.</p>
</aside>
Reference exemplars¶
Good shapes to borrow rather than rebuild:
- A shipped WebGL explainer for the concept you're teaching (foveation, diffusion, whatever) — reference its URL from your post, don't re-derive the math.
- A project-local PermalinkManager utility — one implementation per repo, imported by every demo.
- An audio-reactive shader pipeline elsewhere in the codebase — if a sibling project already solved the AnalyserNode → uniforms plumbing, import or vendor the small hot path instead of starting from scratch.
- A collection of dozens to thousands of images — write a collection document for Wunderkammer 0.2.0 instead of building a wall; see
references/collections.md.
Distribution & hosting¶
Once a demo exists as a single-file HTML, the next decision is where it lives. Different hosts have sharply different strengths — choose based on audience (private vs public), forkability, and build friction.
| Host | Best for | Trade |
|---|---|---|
| GitHub Pages | Repo-hosted demos — canonical, diffable, co-located with source. | Build/push latency; doesn't surface demos for browsing discovery. |
| CodePen | Single-file shareable demos; sci-fi / FUI experiments; "paste this into a Pen" teaching artifacts; fork-to-iterate. | Free Pens are public — don't paste unreleased client or research code. |
| Observable | D3 data-viz figures with reactive cells — the right host for CHI / ETTAC / CIKM paper figures where the reader can move parameters. | Cell-based, not a normal HTML file — different mental model. |
| gist | Minimum-friction single-file distribution; linkable from notes and papers. | No live preview without a third-party renderer (bl.ocks.org went read-only). |
| CodeSandbox / StackBlitz | Full-project demos with npm dependencies; SDK examples. | Overkill for single-file work; heavier iteration loop. |
Local file:// |
Private iteration, screenshot capture, ffmpeg recording of interactive demos. | Doesn't share without promoting to a host. |
CodePen — the Prefill API¶
POST a JSON payload of HTML/CSS/JS to codepen.io/pen/define and open a new Pen with the code pre-populated, ready for the reader to run or fork. Good for "Try this in a Pen" buttons inside blog posts — one click and the reader is in a live editor with your code.
<form action="https://codepen.io/pen/define" method="POST" target="_blank">
<input type="hidden" name="data" value='{
"title": "FUI scan overlay",
"html": "<canvas id=c></canvas>",
"css": "body{background:#000}",
"js": "/* demo code here */",
"tags": ["fui","canvas","webgl"]
}'>
<button type="submit">Open in CodePen</button>
</form>
Full docs: blog.codepen.io/documentation/prefill. Tags, titles, descriptions, and external resources (including marginalia CDN) can all be set in the payload. For educational content, reach for a live fork before a screenshot.
CodePen — embeds¶
Two modes, both work inside marginalia pages:
<!-- Result only -->
<p class="codepen" data-height="400" data-default-tab="result"
data-slug-hash="abcDEF" data-user="your-handle">
See the Pen on CodePen.
</p>
<script src="https://cpwebassets.codepen.io/assets/embed/ei.js" async></script>
<!-- Full editor (html + css + js tabs) -->
<p class="codepen" data-height="600" data-default-tab="html,result"
data-slug-hash="abcDEF" data-user="your-handle">...</p>
Wrap the embed in a marginalia callout for editorial framing inside a blog post:
<aside class="mg-callout" data-type="tip">
<h3>Try it</h3>
<p class="codepen" data-height="500" data-slug-hash="..." data-user="your-handle"></p>
</aside>
Observable — for paper figures¶
Observable notebooks (observablehq.com) are the right host for reactive D3 figures embedded in research papers. Cells are reactive — change one slider and every downstream cell recomputes. The embed runtime (@observablehq/runtime) lets you pull specific cells into a marginalia page, so the notebook becomes the source of truth and the blog post is a curated view into it. Ideal for CHI / ETTAC / CIKM figure supplements where the reader should be able to explore parameters.
Tag conventions¶
On CodePen specifically: tag each experiment with both a technical tag (webgl, canvas, svg, d3) and a genre tag (fui, hud, generative, typography, scrollytelling) so your work surfaces to the right community and theirs surfaces to yours.
Privacy caveat¶
Free CodePen Pens are public. Free Observable notebooks are public. Free gists can be "secret" but are still URL-accessible. Don't paste unreleased shaders, unpublished research data, or client work into any free tier — use GitHub Pages (private repo + gh-pages) or local files for anything sensitive. When in doubt, ask before pasting.
Typographic canvas with pretext¶
chenglou/pretext is a pure JS/TS library for multi-line text measurement and layout without ever touching the DOM. It side-steps getBoundingClientRect / offsetHeight, which trigger layout reflow — one of the most expensive operations in the browser — by implementing its own text measurement against the browser's font engine as ground truth.
For the design lineage behind the typographic ambition here (Cooper, Small, Ishizaki, Maeda → Processing → pretext), see vocabularies/visible-language.md.
When to reach for it¶
- Virtualization / occlusion without height guesstimates — know the exact rendered height of every line before drawing
- Text-in-shape layouts — fill letterforms with body text, flow copy through arbitrary polygonal bands, typeset inside a mask
- Masonry / JS-driven flexbox alternatives where CSS-only layouts hit their limit (or introduce layout shift)
- Canvas / SVG / server-side text where no DOM exists at all
- AI-loop development — measure text outside the browser, verify that labels don't overflow, prevent layout shift when new text loads
Two API tiers¶
Tier 1 — simple paragraph wrap (one string, one font, fixed width):
import { prepareWithSegments, layoutWithLines } from 'https://esm.sh/@chenglou/pretext@0.0.5'
const prepared = prepareWithSegments('AGI 春天到了. بدأت الرحلة 🚀', '18px "Inter"')
const { lines } = layoutWithLines(prepared, 320 /* max width px */, 26 /* line height */)
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i].text, 0, i * 26)
}
Handles Unicode properly — CJK, RTL, emoji, combining characters, the works. prepare* is a one-time cost; layout* is the cheap hot path on resize.
Tier 2 — rich inline (mixed weights, fonts, colors; text flowing through variable-width slots):
import {
prepareRichInline,
layoutNextRichInlineLineRange,
materializeRichInlineLineRange,
} from 'https://esm.sh/@chenglou/pretext@0.0.5/rich-inline'
// Build a list of items: text fragments each with their own font
const items = [
{ text: 'HEADLINER', font: '900 16px Inter' },
{ text: ' · ', font: '400 16px Inter' },
{ text: 'Secondary Act', font: '600 16px Inter' },
// …
]
const colors = ['#ffffff', 'rgba(180,180,200,0.35)', '#4ecdc4', /* … */]
const prepared = prepareRichInline(items)
// Flow through a series of slots (e.g., bands inside a letterform)
let cursor = 0
for (const slot of slots) {
const range = layoutNextRichInlineLineRange(prepared, slot.width, cursor)
if (!range) break
const line = materializeRichInlineLineRange(prepared, range)
for (const frag of line.fragments) {
ctx.font = frag.font
ctx.fillStyle = colors[frag.itemIndex] ?? '#ffffff'
ctx.fillText(frag.text, slot.x + frag.x, slot.y + frag.y)
}
cursor = range.end
}
This is what makes text-in-shape layouts actually work: the library takes your variable-width slots and tells you exactly which rich-inline characters fit in each one, cursor-carrying across slot boundaries.
ESM direct, no build step¶
Pretext publishes as @chenglou/pretext on npm and serves via esm.sh. Import in a single-file HTML demo — matches the muriel no-bundler doctrine:
<script type="module">
import { prepareRichInline } from 'https://esm.sh/@chenglou/pretext@0.0.5/rich-inline'
// …
</script>
Pin the version (@0.0.5 at time of writing) for reproducibility in shipped demos. Pretext is early (0.0.x) and the API is still stabilizing; expect breaking changes if you use @latest.
Font preloading is mandatory¶
Canvas-side text measurement depends on the actual font being loaded. Measure against the fallback and the layout is wrong the instant Inter (or whatever) arrives. Preload before the first call to prepare*:
if (document.fonts?.load) {
await Promise.all([
document.fonts.load('900 200px Inter'),
document.fonts.load('900 13px Inter'),
document.fonts.load('700 13px Inter'),
document.fonts.load('400 13px Inter'),
])
}
This is the Interactive JS-channel version of the Web Rendering channel's "wait for fonts" rule — same failure mode, different timing.
Reference exemplar: pretext-coachella¶
A local worked example of pretext Tier 2 (public source unavailable): pretext-coachella — a 2026 Coachella lineup typeset inside the letterforms of "COACHELLA". Four views over the same 169-artist JSON exercise the whole API surface:
| View | File | Pretext API | What it does |
|---|---|---|---|
| Index | index.html |
none | landing page + cards linking the other three |
| Flow | flow.html |
prepareRichInline + whole-document walk |
all 169 names as one shrinkwrapped paragraph, each word colored by genre family |
| Poster | poster.html |
prepareRichInline + layoutNextRichInlineLineRange + materializeRichInlineLineRange |
the signature view: letters of COACHELLA sliced into bands and slots; artists sorted by tier descending, rendered 500–900 weight, stage-color per artist, justified per slot, live perf stats |
| Wall | wall.html |
prepareWithSegments + layoutWithLines |
perspective wall of individually laid-out artist names |
Reads as a direct descendant of David Small's 1996 Shakespeare navigation: text as navigable space, multi-scale rendering via tier-based weight (instead of Small's greeking), categorical color for structure, Canvas2D at 60fps because pretext measures outside the DOM reflow path. Good scaffold for any new typographic-canvas work.
Radial-menu shape flow¶
For pie or marking menus, keep the host's SVG or DOM wedge as the semantic target and use Pretext as the renderer-independent text compositor. First lock separate status, command-glyph, label, and exact-value rails. For horizontal copy, intersect text rails with the padded annular sector. For an explicit arc-follow probe, give Pretext the guarded arc length at each candidate label radius; do not wrap against a chord and bend the line afterward. Select one legal label tier across the peer ring; do not let short labels grow or status glyphs squeeze their local wedge. Routine state is glyphed, only decision-changing values remain wedge text, and neither is appended inline to the command label. Target paths remain byte-stable across hover and focus.
The default hybrid for existing SVG wheels is normal positioned DOM content over
stable SVG targets. This follows iBlipper's production pattern: Pretext predicts
line breaks and the wheel-wide type tier, ordinary DOM carries the actual
typography, and a settled getBoundingClientRect() pass checks rendered truth,
peer type variance, glyph/text rail gaps, and label/value separation.
An arc-follow renderer materializes the winning Pretext fragments as individual grapheme spans. Preserve kerning with cumulative-prefix measurement and reparameterize the line-to-arc blend so Cartesian interpolation cannot contract letter spacing on side wedges. Keep path bow and per-letter tangent turn as independent probe parameters, reverse traversal to keep lower-half text upright, and leave command and status glyphs unrotated. Start near 30% bow and 10% orientation follow; a 40-degree command word is a readability failure, not a target. The complete command remains on the semantic wedge and the per-letter DOM layer is visual projection.
Carrier selection precedes decoration. Short-label rings may use the shallow DOM
bow. Value-dense rings and multiword commands use upright word lines, with the
full source command retained in the accessible name and caption when a shorter
visible displayLabel is needed.
Load references/radial-menus.md for the full
state, geometry, typography, glyph, keyboard, touch, stop-condition, and proof
contract. The runnable
examples/radial-menu-pretext.html
compares fixed midpoint SVG stacks with an adaptive carrier: a vendored Pretext
0.0.5 shallow per-grapheme bow for Psychodeli and an upright value-dense stack
for Pot Luck.
Sci-fi UI (FUI) patterns¶
When the output wants to feel like telemetry — live stats, mission clocks, bearing readouts, signal traces — reach for the FUI vocabulary. The full grammar, substrate decision table, and common-failure list live in vocabularies/fui.md; this section is the interactive-channel seam.
A circular silhouette is not automatically a radial menu. Before rendering a
ring-heavy FUI, declare what angle, radius, centre, each ring, and each motion
mean. Load
references/annular-interfaces.md for the
shared polar contract and its distinction between directional commands, sensor
plots, gauges, cyclic timelines, hierarchies, and reticles. Load the stricter
references/radial-menus.md only when sectors
are commands.
Canonical stack (lifted from the vocabulary's substrate decision):
- HTML + CSS Grid for layout and text, with
font-variant-numeric: tabular-numson any changing digits. - SVG for thin-stroke geometry — compass rings, corner brackets, grid overlays, leader lines.
- Canvas2D + rAF for tickers, waveforms, and anything that re-draws per frame.
- CSS
repeating-linear-gradientfor scan-line overlays; WebGL2 fragment shader if the post-process needs to vary spatially (glitch, chromatic aberration). - Motion from muriel's brand tokens:
--mg-duration-reveal,--mg-ease-emphasis, plus a project-local--mg-stagger(100 ms).
Runnable scaffold: examples/fui-scaffold.html. Open in a browser. Single file, no build. Four primitives on one screen:
- Data ticker — rAF counter with leading zeros, mission clock, packet hex.
- Radial compass — SVG rings + 36 tick marks + rotating needle.
- Canvas waveform — scrolling signal trace with RMS/peak readouts.
- Staggered reveal — CSS animation across the four blocks at 100 ms intervals.
Runnable circular-instrument case:
examples/annular-fui-console.html.
Angle is bearing, radius is range, and the centre is a simulated sensor origin.
The SVG polar plot and HTML contact list share one deterministic record set;
pointer hover previews, click pins, keyboard steps by declared bearing/range
semantics, and the phone view promotes the linear equivalent when embedded SVG
labels would fall below the physical type floor. The sweep is sampling feedback,
not a control or ambient animation.
Corner brackets via ::before / ::after. Scan-line overlay via repeating-linear-gradient. prefers-reduced-motion fallback baked in. Every text element clears 8:1 against #0a0a0f. Swap the four CSS custom properties at the top for a brand's palette and the composition inherits.
CodePen Prefill the scaffold to hand readers a live fork — see the Prefill API section above. Useful when a blog post describes a FUI primitive; one click and the reader is in a live editor.
Lessons from past projects¶
- No bare
console.log. Route through a debug manager or conditional logger so shipped demos stay clean. - Don't dim/fade search-result cards on hover or brush. It breaks scanability more than it rewards the hovered row.
- WebGL variable names: read the source, don't guess.
- Shader changes need verification. Run tests or capture a screenshot before claiming a fix is working.
- Pretext: always pin
@x.y.z, never@latest, in shipped demos. - Pretext: always preload fonts before the first
prepare*call, or the measurement is against the fallback font.
Anti-patterns¶
- Don't set up listeners or rAF loops without cleanup. Memory leaks on hot reload; double-fires on navigation.
- Don't poll state in a rAF loop when an event listener would do. Events are cheaper and correct by construction.
- Don't use
alert()inside a demo. It halts the demo's timing and disrupts screen-capture pipelines. - Don't hardcode parameter defaults outside PermalinkManager. Shared-state demos silently diverge when defaults live in two places.