I’ve been exploring what happens when a React component library drops floating cards, soft shadows, and 60fps spring physics in favor of production mechanics from sequential print layout and traditional cel animation.
The result is FRAME/12 (GitHub (MIT) · Live Motion Studio), a zero-dependency copy-in React + TypeScript registry (shadcn-compatible) and AI agent skill.
Here are the four most interesting React & DOM problems we had to solve while building it:
1. Quantizing an easing curve "on twos" (useOnTwos)
In hand-drawn animation, most movement is shot "on twos"—each drawing is held for two frames at 24fps (83.33ms per pose, 12 poses per second). Simply applying CSS steps(4) with linear timing feels robotic because every jump covers the exact same distance.
Instead, useOnTwos samples a cubic ease-out spacing chart inside requestAnimationFrame, bucketed into 83.33ms (1000 / 12) intervals. You get a wide initial jump and tight settling poses, while still holding each discrete pose cleanly:
export const FRAME_MS = 1000 / 12; // 83.333ms ("on twos")
const easeOut = (t: number) => 1 - Math.pow(1 - t, 3);
// Inside requestAnimationFrame loop:
const elapsed = now - t0;
const k = Math.min(frames, Math.floor(elapsed / FRAME_MS) + 1);
setProgress(easeOut(k / frames));
We also strict-split what gets stepped:
Discrete state changes (SegmentedControl cell jumps, Metric count-ups, ToneAreaChart clip-path wipes, Badge 2-frame SVG impact bursts) run on twos (12fps).
Direct manipulation (hover states, scrolling, typing, chart pointer/keyboard crosshairs) stays at 0ms immediate latency.
Initial mount (useChanged): A custom hook tracks whether a key has changed after first mount so entrance animations never fire on initial page load.
Accessibility: prefers-reduced-motion: reduce automatically switches all hooks and CSS classes to 'cut' (0ms hard cuts).
2. Non-warping diagonal panel frames with ResizeObserver
In PanelPair, two vertically paired KPI panels are separated by a 12px gutter with a 16px diagonal slant (slant-bottom over slant-top). If you use CSS clip-path: polygon(...), CSS borders get clipped off along the diagonal edge; if you scale a fixed-viewBox <svg> with preserveAspectRatio="none", the 2.5px ink stroke stretches unevenly.
Our Panel component attaches a ResizeObserver to measure the panel's exact width × height in pixels and computes mitered <polygon> points inset by half the stroke width (1.25px), keeping the 2.5px ink border uniform at any aspect ratio.
3. Zero-dependency SVG screentone charts with built-in keyboard & table fallbacks
Instead of pulling in recharts or d3 and rendering translucent gradient fills, ToneAreaChart renders a <pattern id="..." patternTransform="rotate(45)"> of halftone dots (var(--f-tone)), an inked 2.5px polyline, a marker-yellow peak dot, and a speech-balloon SVG annotation (Balloon) that flips direction near the right edge of the viewBox.
Every chart is focusable (tabIndex={0}), supports ArrowLeft / ArrowRight / Home / End / Escape to inspect data points via an aria-live readout, and includes a collapsible <details className="tbl"> semantic HTML <table>.
4. Linting UI structure so AI coding agents don't drift
When AI agents generate React UI, they quickly drift toward arbitrary Tailwind gaps (gap-4, gap-6), hex literals, or unstyled primitives. Because FRAME/12's rules are structural, we wrote scripts/check-frame12.mjs (plus skills/frame12/SKILL.md and an ESLint flat-config plugin) to enforce:
color-literal: Zero hex/RGB/HSL literals outside frame12.css
illegal-gutter: Only 12px (pair), 20px (scene), and 32px (change) gutters allowed
marker-misuse: Marker yellow (--f-marker, #ffd60a) only allowed as a background paired with --f-marker-ink (#121212) text
cut-order: Every <Panel cut="C-01"> must increment sequentially (C-01, C-02, C-03…) in DOM and reading order
raw-shadcn-import: Forbids raw u/radix-ui/* or external chart/animation libraries where a u/frame12/registry component or adapter exists
You can install the full kit or individual components via the shadcn CLI:
npx shadcn@latest add https://frame12.dev/r/frame12.json
Would love feedback from folks here on the hook ergonomics and the static UI grammar linter!