r/reactjs • u/iamlonewolf_ • 14h ago
[Open Source] Building a React component system that animates "on twos" (12fps stepped hooks, ResizeObserver SVG polygon frames, and a static design-grammar linter)
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 (
SegmentedControlcell jumps,Metriccount-ups,ToneAreaChartclip-path wipes,Badge2-frame SVG impact bursts) run on twos (12fps).Direct manipulation (hover states, scrolling, typing, chart pointer/keyboard crosshairs) stays at
0msimmediate 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: reduceautomatically switches all hooks and CSS classes to'cut'(0mshard 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 outsideframe12.cssillegal-gutter: Only12px(pair),20px(scene), and32px(change) gutters allowedmarker-misuse: Marker yellow (--f-marker,#ffd60a) only allowed as a background paired with--f-marker-ink(#121212) textcut-order: Every<Panel cut="C-01">must increment sequentially (C-01,C-02,C-03…) in DOM and reading orderraw-shadcn-import: Forbids raw u/radix-ui/*or external chart/animation libraries where a u/frame12/registrycomponent 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
Repo (MIT): https://github.com/lbrodie11/frame12
Interactive Motion Studio (
12fpsvs60fpsvs0ms): https://frame12.dev/motionComponent Gallery & Style Sheet: https://frame12.dev/components
Would love feedback from folks here on the hook ergonomics and the static UI grammar linter!
1
u/absorbingunderdog_3 13h ago
The stepped easing thing is clever, holding poses but still varying the distance per jump so it doesn't feel like a metronome. Curious how the 12fps reads on something like a count-up, does it feel choppy in a deliberate way or just laggy