Rules. What keeps it honest.
Each rule was written after a real bug. The AGENTS.md generator on the home page reads this same list.
do Use a CSS variable for every color
don't Hardcode a hex in a component. It stops following the theme
do Derive brand accents from --fg-brand with color-mix()
don't Hardcode the violet. It stops reacting when someone picks another theme
do Put --fg-on-brand on a brand fill
don't Reach for a fixed near-white. It fails contrast in 7 of the 12 theme and mode pairs
do Color brand text below 24px with --fg-brand-text
don't Use --fg-brand for small text. It fails AA in 4 of the 12 pairs
do Put cards on --bg-card, menus on --bg-surface, dialogs on --bg-overlay
don't Fill a grid of cards with --bg-surface. It reads as a field of gray
do Size text with the ten scale steps, such as text-mono-sm
don't Write text-[13px] or Tailwind's text-sm. A test fails on both
do Pick the family at the call site: font-serif, font-sans or font-mono
don't Expect a scale step to set the font. It sets size and leading only
do Let a row of two halves wrap, and keep each half on one line
don't Assume it fits. A card clips with no ellipsis, so overflow looks like missing data
do Trigger entrances with whileInView and once, everywhere
don't Use animate above the fold. One trigger leaves no judgement call to get wrong
do Ask useReducedMotion() in anything animated through JS
don't Assume the CSS reset caught it. Motion never goes through that block
do Keep the real text in the DOM and animate a hidden copy
don't Grow a string in state. Crawlers and screen readers get an empty heading
do Move glows and shines with transform
don't Rebuild a gradient every frame. It repaints the card and stalls other animations
do Hide glyphs such as ◆, $ and // with aria-hidden
don't Let a screen reader say black diamond suit before every label
do Give bare buttons the .focus-ring class
don't Rely on outline. The reset removes it from every button
do Import Phosphor from @phosphor-icons/react/dist/ssr in files without use client
don't Import the package root there. next build fails with an error that names no file
do Pass your router's link with asChild or linkComponent, and mark the current item with active
don't Import next/link in a copied component. It stops working outside Next.js