Skip to content

Reference · components

Audit checklist: 16 points for registry/component-library source

components/references/audit-checklist.md233 linesupdated 16 Sept 2026

Run this after fetching a registry item's JSON (SKILL.md → fetch commands) and before rewriting it. Every fail becomes one line in the rewrite plan (template at the bottom). rg/jq here are triage greps over the fetched source — fast, but pattern-matching, not real parsing. void lint . runs the same checks as real AST rules on committed files and is the ground truth; use it once the rewrite lands in the repo. Bug write-ups per pattern (with the library/component that shipped them): references/upstream-bugs.md. Paste-ready replacements: references/recipes.md.

Re-verify rule ids before citing them elsewhere — the rules files change:

bash
grep -n 'id: "' packages/cli/src/rules/*.ts          # design/seo/perf/a11y/geo ids, verbatim
grep -oE 'r\("[a-z0-9-]+"' packages/cli/src/rules/lint.ts   # lint/* ids (prefix is added by the r() helper)
bun packages/cli/src/index.ts rules <id>              # print one rule's why/fix/example
bun packages/cli/src/index.ts rules                   # list every rule

Registry item metadata in one shot:

bash
curl -sL <item>.json | jq '{dependencies, registryDependencies, css, cssVars}'
curl -sL <item>.json | jq -r '.files[].content' > /tmp/item.tsx   # source to a scratch file for rg
diff <(jq -r '.dependencies[]' /tmp/item.json) <(jq -r '.dependencies|keys[]' package.json)  # phantom/renamed deps

1. Licence

Check: which licence class the item's source repo/registry falls under, and what the actual file header says (registries sometimes ship code the docs site doesn't mention).

bash
head -20 /tmp/item.tsx | grep -i 'copyright\|license'
curl -sL <repo>/LICENSE            # or LICENSING.md (coss/Origin UI), LICENCE.md (motion-primitives)

Pass: MIT/Apache/Unlicense → may vendor with notice kept. Commons Clause (React Bits, Animate UI), proprietary (Aceternity, Hover.dev) or attribution-required (Skiper free) → clean-room only. Full matrix: SKILL.md → Licence matrix, references/catalogue.md. Rule: none (legal, not lintable). Fix: MIT-class → vendor + fix every other fail below, keep the header. Anything else → write the 5–10 line spec (SKILL.md → Rewrite onto void tokens), close the source, build clean-room.

2. SSR safety

Check: browser globals or non-deterministic values evaluated at module scope or during render (outside useEffect/event handlers).

bash
rg -n 'window\.|document\.|navigator\.|matchMedia\(|localStorage' /tmp/item.tsx
rg -n 'Math\.random\(\)|Date\.now\(\)|new Date\(\)' /tmp/item.tsx
rg -n -B2 'window\.|Math\.random' /tmp/item.tsx | rg -v 'useEffect|addEventListener\('   # crude: flags hits not near an effect

Pass: zero hits outside useEffect/useSyncExternalStore/event handlers. Rule: lint/browser-global-in-render, lint/random-in-render. Fix: read browser APIs in an effect or useSyncExternalStore (server snapshot = the neutral default); replace Math.random() with a value seeded from the index. Pattern group: references/upstream-bugs.md §3.

3. Server HTML is complete

Check: headline, numbers and links are present and visible in the raw HTML, not hidden until JS runs.

bash
rg -n 'opacity-0|opacity:\s*0|initial=\{\{\s*opacity:\s*0|initial="hidden"' /tmp/item.tsx
curl -s <dev-url> | grep -oE '<h1[^>]*>.*</h1>'          # headline present, no opacity-0 class
curl -s <dev-url> | grep -o '12,400'                      # the real number, not "0" or empty
curl -s <dev-url> | grep -c '<a '                          # links present without JS

Pass: real content in the curl output; no opacity-0/initial="hidden" on above-the-fold text; a numeric component never SSRs 0/"". Rule: none static (grep + curl only); the runtime symptom shows up as perf/lcp-slow once it ships. Fix: SSR the final value/text and animate position only (RevealText), or animate a number only if it starts offscreen (NumberTicker). Pattern group: references/upstream-bugs.md §2.

4. Cleanup is 1:1

Check: every subscription, observer, timer or library instance has a matching teardown.

bash
rg -c 'addEventListener\(' /tmp/item.tsx;      rg -c 'removeEventListener\(' /tmp/item.tsx
rg -c 'requestAnimationFrame\(' /tmp/item.tsx; rg -c 'cancelAnimationFrame\(' /tmp/item.tsx
rg -c 'setInterval\(|setTimeout\(' /tmp/item.tsx; rg -c 'clearInterval\(|clearTimeout\(' /tmp/item.tsx
rg -n 'new Lenis\(|createGlobe\(' /tmp/item.tsx; rg -n '\.destroy\(\)' /tmp/item.tsx
rg -c '\.on\(' /tmp/item.tsx;                   rg -c '\.off\(' /tmp/item.tsx

Pass: each "add" count has a matching "remove" count (not just present somewhere — check it's in the same effect's cleanup return). Rule: lint/listener-without-cleanup, lint/raf-without-cancel, lint/timer-in-render, lint/unload-listener (an unload listener specifically blocks bfcache). Fix: return the remove/disconnect/cancel/clear/destroy from the same useEffect, or pass { signal } from one AbortController and abort it. Pattern group: references/upstream-bugs.md §4.

5. Hooks correctness

Check: hooks called unconditionally at the top level; no component/MotionValue-subscription creation during render; stable effect deps.

bash
rg -n '&&\s*<.*use[A-Z]\w*\(|\?\s*use[A-Z]\w*\(|style=\{\{[^}]*use[A-Z]\w*\(' /tmp/item.tsx   # AST hint, approximate: use[A-Z]… inside &&/ternary/JSX literal
rg -n 'm\.create\(|createElement\(' /tmp/item.tsx        # component defined/created in render
rg -n '\.on\((["'\'']change)' /tmp/item.tsx              # MotionValue .on outside useMotionValueEvent
rg -n '\[.*ref\.current.*\]' /tmp/item.tsx                # ref.current in a deps array

Pass: no hooks inside &&/ternary/JSX prop literals; m.create()/component factories only at module scope or useMemo; MotionValue subscriptions via useMotionValueEvent; refs never appear inside [] deps. Rule: lint/conditional-hook, lint/motionvalue-subscribe-in-render, lint/svg-global-id (a related AST smell: a hardcoded SVG id instead of useId(), breaks the 2nd instance on the page). Fix: hoist the hook call, feed the result in conditionally; useMotionValueEvent(value, 'change', cb); useId()-scope any SVG / id. Pattern group: references/upstream-bugs.md §5.

6. Zero per-frame React work

Check: no setState inside a pointer/scroll/rAF handler; no allocation inside a loop.

bash
rg -n -B3 'set[A-Z]\w*\(' /tmp/item.tsx | rg -B3 'onPointerMove|onMouseMove|onScroll|requestAnimationFrame'   # AST hint: setState reachable from a per-frame handler
rg -n -B2 'new Intl\.NumberFormat|new Color\(' /tmp/item.tsx    # then confirm the call site is inside rAF/.on('change')/useAnimationFrame
rg -n 'getBoundingClientRect\(\)' /tmp/item.tsx                 # forced layout reads; flag if inside a loop/handler

Pass: pointer/scroll paths write to a ref's style/CSS custom property or a MotionValue .set(), never setState; formatters/colour objects built once (module scope or useMemo), not per frame. Rule: lint/setstate-per-pointer-move, lint/allocation-per-frame. Fix: ref + one rAF writing el.style.translate/--x (recipe 2 SpotlightCard, recipe 7 Magnetic); hoist Intl.NumberFormat to useMemo. Pattern group: references/upstream-bugs.md §6.

7. Reduced motion

Check: the file honours the OS preference, and the honouring actually disables the loop (not just present).

bash
rg -c 'prefers-reduced-motion|useReducedMotion|motion-reduce:|motion-safe:' /tmp/item.tsx

Pass: count > 0, and manually confirm it gates the actual animation (a global CSS reduced-motion rule alone doesn't cover JS rAF loops or inline style transforms — those need their own check). Rule: lint/no-reduced-motion, smooth/reduced-motion-ignored. Fix: a static finished frame with content intact; disable via motion-reduce:/@media (prefers-reduced-motion: reduce) for CSS, useReducedMotion()/ for JS. This is the single most common fail (~95% of registry items) — never skip it. Cross-cutting across every group in references/upstream-bugs.md.

8. Offscreen pause

Check: infinite animations stop when off-screen; JS loops also stop on tab hide.

bash
rg -n 'IntersectionObserver|useInView|data-fx' /tmp/item.tsx
rg -n 'visibilitychange' /tmp/item.tsx    # JS rAF/WebGL loops only
rg -n 'repeat:\s*Infinity|infinite\b' /tmp/item.tsx   # candidates that need one of the above

Pass: every repeat: Infinity/infinite/rAF loop is paired with an IntersectionObserver, useInView, or the shared [data-fx] gate; JS loops also react to visibilitychange. Pausing must cancel the loop (cancelAnimationFrame), not just skip drawing — a skipped-but-still-scheduled rAF still burns a callback every frame. Rule: smooth/raf-loop-idle. Fix: CSS-driven effects get data-fx + the shared FxGate (recipes.md §0); JS/WebGL loops cancel on visibilitychange and IntersectionObserver both. Pattern group: references/upstream-bugs.md §7.

9. DPR / resolution cap

Check: canvas/WebGL pixel ratio is capped, not raw or hardcoded.

bash
rg -n 'devicePixelRatio' /tmp/item.tsx
rg -n 'devicePixelRatio\s*[,)]\s*1\.5|Math\.min\(.*devicePixelRatio' /tmp/item.tsx   # the capped form
rg -n 'devicePixelRatio:\s*2|width\s*\*\s*2|height\s*\*\s*2' /tmp/item.tsx           # hardcoded 2x (Magic UI globe)

Pass: Math.min(devicePixelRatio, 1.5) (1 for full-bleed backdrops) read inside an effect, never a bare or hardcoded 2; full-bleed shader render scale ≤0.6. Rule: lint/hardcoded-device-pixel-ratio. Fix: cap and resize via ResizeObserver, never at module scope. Pattern group: references/upstream-bugs.md §10.

10. Frame-rate cap

Check: ambient T3/T4 loops throttle to ≤30fps instead of running the full rAF rate.

bash
rg -n 'requestAnimationFrame' /tmp/item.tsx    # then read the callback: does it throttle, or draw every frame unconditionally?
rg -n 'frameloop\s*=\s*.always.|fpsLimit|1000\s*/\s*[0-9]+' /tmp/item.tsx

Pass: ambient backgrounds (aurora, globes, particle fields) skip frames to land at ≤30fps; foreground/interactive canvas may run full rate. No static grep proves this — read the draw loop. Rule: none static; verify with void smooth --routes (dropped-frame % and the profiler timeline). Fix: accumulate elapsed time and only draw past a 1000/30 threshold. Pattern group: references/upstream-bugs.md §10.

11. Properties animated

Check: continuous motion touches only transform/opacity; anything else is one-shot or a single small element.

bash
rg -n '@keyframes' -A6 /tmp/item.tsx   # read the keyframe body
rg -n 'width:|height:|top:|left:|margin|padding' /tmp/item.tsx        # layout properties in an animated block
rg -n 'background-position|filter:|box-shadow|linearGradient.*x1|y1' /tmp/item.tsx   # paint properties
rg -n 'transition:\s*all|transition-all\b' /tmp/item.tsx

Pass: no width/height/top/left/margin/padding inside a repeat/infinite block; paint properties (filter, background-position, box-shadow, SVG gradient coordinates) animate once or on one small element only, never continuously full-bleed; no transition: all. Rule: lint/animate-layout-prop, smooth/animate-layout-property, lint/transition-all, smooth/transition-all. Fix: translate/scale/rotate/opacity only; grid-template-rows: 0fr → 1fr for accordions; a static overlay instead of mask-image over animated children (measured: 10% dropped frames); will-change: transform on the one paint-animated element, never many. Pattern group: references/upstream-bugs.md §8 — also see the recipes.md aurora/shine gotcha (never stack a paint animation over an animated backdrop: 14–46% dropped frames measured).

12. Accessibility

Check: decorative layers hidden; duplicated content hidden from AT; real interactive elements; hover has a keyboard/focus equivalent; long-moving content can pause.

bash
rg -c '\.map\(' /tmp/item.tsx                                    # candidates for duplicated marquee/loop content
rg -n 'aria-hidden' /tmp/item.tsx                                 # present on every duplicate + every decorative layer?
rg -n 'inert' /tmp/item.tsx                                       # duplicates need aria-hidden AND inert (aria-hidden alone still leaves links focusable)
rg -n '<div[^>]*onClick|<span[^>]*onClick' /tmp/item.tsx          # clickable non-interactive element
rg -n '<button(?![^>]*type=)' /tmp/item.tsx                       # <button> missing an explicit type (approximate)
rg -n ':hover|whileHover|group-hover' /tmp/item.tsx;  rg -n 'focus-visible|:focus\b' /tmp/item.tsx   # hover without a focus counterpart

Pass: decorative layers aria-hidden; duplicated content (marquee copies, TextRoll) has both aria-hidden and inert; clickable elements are real /

rg -n '@media\s*\(hover:\s*hover\)|hover:hover.*pointer:fine' /tmp/item.tsx
rg -n 'pointerType' /tmp/item.tsx
rg -n ':hover|whileHover|group-hover' /tmp/item.tsx    # candidates that should be inside the gate above

Pass: every hover-triggered animation sits under @media (hover: hover) and (pointer: fine) (Tailwind: hover:hover: variant stacking, or a plain media query in CSS); pointer event handlers branch on e.pointerType !== 'touch' where a mouse-only effect (magnetic, spotlight) would otherwise fire from a tap. Rule: none dedicated; covered by manual review plus lint/setstate-per-pointer-move for the handler itself. Fix: wrap the CSS in the media query; check pointerType before running pointer-follow logic. Pattern noted in references/upstream-bugs.md §11 (hover-only pause on the marquee is the same root cause: hover semantics applied without a touch/keyboard fallback).

14. Dependency weight and duplicates

Check: the item's dependencies fit the target tier, aren't phantom/renamed, and don't duplicate a library already in the app.

bash
jq '.dependencies' /tmp/item.json
diff <(jq -r '.dependencies[]?' /tmp/item.json | sort) <(jq -r '.dependencies|keys[]' package.json | sort)   # phantom deps not in package.json
rg -n 'framer-motion' /tmp/item.tsx     # duplicate runtime if the app already uses `motion`
rg -n '@base-ui-components/react' /tmp/item.tsx   # renamed package (now @base-ui/react) — duplicate, stale
rg -n 'three|@react-three' /tmp/item.tsx          # three/R3F for what might be a 2D quad
rg -n 'face-api' /tmp/item.tsx                    # ~700KB bundle bomb (React Bits GridScan)

Pass: deps map to the tier the effect actually needs (nothing/CSS → T1, motion → T2, getContext('2d') → T3, ogl/three/cobe → T4 — see SKILL.md tier table); no framer-motion next to motion, no @base-ui-components/react next to @base-ui/react, no three.js for a single 2D quad. Rule: lint/heavy-import, smooth/multiple-webgl-contexts, lint/webgl-in-map (a WebGL canvas rendered inside .map() — browsers evict contexts past ~8–16 per page). Fix: down-tier (WebGL Aurora → CSS gradients, recipe 9) or swap for the matching library (@number-flow/react, cobe, torphSKILL.md decision tree step 3). Also watch for DOM/node explosions from the same audit pass (Array(n).map() of animated nodes, e.g. Magic UI dot-pattern's ~5,130 s) — same fix direction, lower node count via CSS. Pattern groups: references/upstream-bugs.md §9, §10.

15. Tokens

Check: no raw colour literals or default Tailwind palette classes; motion timing uses tokens, not magic numbers.

bash
rg -n '#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(' /tmp/item.tsx
rg -n '\b(neutral|zinc|gray|slate|purple|indigo|violet|pink)-[0-9]{2,3}\b' /tmp/item.tsx
rg -n 'duration-\[[0-9]|ease-\[cubic-bezier|[0-9]{2,4}ms' /tmp/item.tsx   # hardcoded durations/eases instead of --duration-*/--ease-*

Pass: every colour is a var(--color-*)/semantic utility (bg-surface, text-fg-muted, border-line); no default-palette classes; motion timing reads --ease-*/--duration-*. Rule: lint/hardcoded-colors, design/purple-gradient (the violet→indigo hero trope), design/gradient-text (background-clip:text headlines), design/multiple-accents. Fix: run the registry→void token table in SKILL.md → Rewrite onto void tokens; one hue via var(--brand) + color-mix(in oklab, var(--brand) 20%, transparent).

16. Tailwind v4 correctness

Check: v3-only syntax that silently compiles to nothing or the wrong value under v4.

bash
rg -n 'bg-gradient-to-|flex-shrink-0|flex-grow-0' /tmp/item.tsx
rg -n 'duration-\[--|ease-\[--|[a-z-]+-\[--[a-z-]+\]' /tmp/item.tsx     # v3 `[--var]` arbitrary-var syntax; v4 needs `(--var)`
rg -n '\bshadow\b(?!-)|\brounded\b(?!-)|\boutline-none\b' /tmp/item.tsx  # bare/renamed v3 utilities
rg -n 'animate-[a-z-]+' /tmp/item.tsx                                    # then confirm each has matching css/cssVars (jq check above) or a `--animate-*` in @theme
ls tailwind.config.* 2>/dev/null && rg -L '@config' src/**/*.css        # JS config present but never loaded from the CSS entry

Pass: bg-linear-to-* not bg-gradient-to-*; shrink-0 not flex-shrink-0; duration-(--x) not duration-[--x]; shadow-xs/rounded-xs/outline-hidden (v4 renames); every custom animate-* class has matching CSS shipped with the item or defined in @theme; a tailwind.config.*, if present, is loaded via @config or has been migrated into @theme. Rule: lint/tw-unknown-class, lint/tw-v3-arbitrary-var, lint/tw-v3-renamed, lint/tw-js-config-ignored. Fix: rename per the v3→v4 map in SKILL.md → Rewrite onto void tokens; move keyframes into @layer components or @theme; next build and grep the compiled CSS for the class to confirm it exists. Pattern group: references/upstream-bugs.md §1.

Rewrite plan template

Fill this in per item, in the PR/commit description, after running all 16 points:

# Point Fail found Fix applied Rule id(s) closed
2 SSR safety navigator.userAgent at module scope moved into useSyncExternalStore, server snapshot false lint/browser-global-in-render
7 Reduced motion no prefers-reduced-motion anywhere added a finished static frame + motion-reduce: lint/no-reduced-motion, smooth/reduced-motion-ignored
12 Accessibility 3 marquee copies, no aria-hidden/inert aria-hidden={i>0} + inert={i>0} on copies 2–3 lint/duplicate-children-not-hidden
15 Tokens #9c40ff → #ffaa40 gradient one hue via var(--brand) lint/hardcoded-colors, design/purple-gradient

An item with zero fails after a real MIT-class licence may be vendored as-is (still keep the copyright header). Anything with a licence fail skips straight to clean-room regardless of how many other points pass.