Design system constraints

On-brand rules the agent enforces by default, plus the protocol for proposing new components

The reva-design-system-adherence skill auto-fires across the whole monorepo. These are the rules it enforces by default, mirrored here for human reference. The canonical source is .agents/skills/reva-design-system-adherence/SKILL.md; the mechanical ones are also checked automatically by the design audit (bun run audit:design) in CI and the pre-commit hook — see Design system MCP.

Hard constraints

Color

Use design tokens, never arbitrary hex values, and not Tailwind's default color scale unless the color is explicitly part of the Reva palette.

// ✅ OK
<div className="bg-primary-solid text-primary-fg-onSolid" />
<Badge variant="subtle" intent="constructive">Active</Badge>
<Alert status="warning" />

// ❌ Not OK
<div className="bg-[#fbbf24] text-white" />
<div className="bg-blue-500 text-white" />

The Reva palette includes custom OKLCH ramps for hay-*, amber-*, mulberry-*, gold-*, fern-*, plus olive-* (which stays available because it isn't overridden). Default Tailwind names like slate-*, zinc-*, red-*, green-*, blue-* are either remapped or out of scope.

Component registry

Before building anything new, check @reva/ui exports. The barrel is at packages/ui/src/index.ts. If a primitive covers the use case, use it.

Common primitives to reach for: Card, Button, IconButton, Tabs, ToggleGroup, Sidebar, Sheet, Dialog, AlertDialog, Form, Field, Table, Chart, Accordion, Empty, Spinner, Toggle, Badge, StatusBadge, Alert, Skeleton, Carousel, Shelf, Avatar, Item, Combobox, Select, Input, Textarea, Checkbox, RadioGroup.

Rows are Item, not Alert

Any labelled row — a settings toggle, notification, connected account, team member, file, menu entry, or navigable link — is an Item. Don't restyle an Alert (which is for transient status messages) or hand-roll a Card + flex <div> to get this shape. Buttons inside ItemActions default to size="sm" — the default Button is too tall for a row.

// ✅ OK — a settings row
<Item variant="outline">
  <ItemMedia variant="icon"><ShieldCheck /></ItemMedia>
  <ItemContent>
    <ItemTitle>Two-factor authentication</ItemTitle>
    <ItemDescription>Add a second step when signing in.</ItemDescription>
  </ItemContent>
  <ItemActions>
    <Button size="sm">Enable</Button>
  </ItemActions>
</Item>

// ❌ Not OK — an Alert bent into a row, or a hand-rolled div
<Alert>
  <ShieldCheck />
  <div className="flex-1">Two-factor authentication…</div>
  <Button>Enable</Button>
</Alert>

Variant API

Variants on existing primitives must match the existing shadcn/cva variant shape exactly. No new variants without being explicitly asked. If a variant is missing for what you're trying to do, propose a new component instead (see Proposing new components).

Layout primitives

Use the layout primitives over raw flex/grid <div>s, and use the typography primitives over raw heading/text tags with className.

// ✅ OK
<Stack gap={4}>
  <Heading as="h2" size="2xl">Title</Heading>
  <Text color="muted-foreground">Subtitle</Text>
</Stack>
<HStack align="center" justify="between">…</HStack>
<Grid columns={{ default: 1, lg: 3 }} gap={6}>…</Grid>

// ❌ Not OK
<div className="flex flex-col gap-4">
  <h2 className="text-2xl font-semibold">Title</h2>
  <p className="text-fg-muted">Subtitle</p>
</div>

Eyebrows and small labels

Stat captions and section eyebrows are sentence case — never all-caps. uppercase / tracking-wider on a label reads as a louder, off-brand system (the uppercase-eyebrow audit rule).

// ✅ OK
<Text textStyle="caption1" color="muted-foreground">Net worth</Text>

// ❌ Not OK
<Text className="text-xs uppercase tracking-wider text-fg-muted">Net worth</Text>

Card padding

Card owns its own vertical padding (py-6); CardContent / CardHeader / CardFooter add horizontal padding only. Never add py-* (or an all-sides p-*) to a card slot — it stacks on the Card's py-6 and doubles it (the cardcontent-padding audit rule).

// ✅ OK
<CardContent className="flex flex-col gap-4 px-4">…</CardContent>

// ❌ Not OK — doubles the block padding
<CardContent className="px-4 py-4">…</CardContent>

Dialog footers follow one recipe: actions group at the end (no justify-between, no mr-auto split, no full-width buttons), secondary action before primary. Variants by role: the primary action (Save, Done, Confirm) is a solid default Button; cancel / close / reset-to-defaults is variant="outline"; a destructive action (Delete) is solid variant="destructive", placed before the primary. Footer buttons keep the default size — never size="sm" in a dialog footer.

// ✅ OK — outline secondary, solid primary, default sizes, grouped at the end
<DialogFooter>
  <Button variant="outline">Cancel</Button>
  <Button type="submit">Save</Button>
</DialogFooter>

// ❌ Not OK — sm buttons, split footer
<DialogFooter className="justify-between">
  <Button variant="outline" size="sm" className="mr-auto">Cancel</Button>
  <Button size="sm">Save</Button>
</DialogFooter>

Icons

@phosphor-icons/react/ssr only. Always import from /ssr (not the bare entry — the bare entry uses React.createContext which Next.js App Router RSC doesn't expose).

import { CaretDown, House } from "@phosphor-icons/react/ssr";

Drop the …Icon suffix on names — Phosphor uses bare names (House, not HouseIcon).

Aesthetics

The Reva visual language is flat, warm-neutral surfaces with amber/gold accents and subtle motion. Avoid:

  • White-elevated-cards-with-large-drop-shadow patterns (off-brand)
  • Generic SaaS gradients (off-brand)
  • Liquid-glass / Apple mimicry (off-brand)
  • Overly dense, table-and-button-heavy layouts (VISUAL_DENSITY is dialed to 5 — balanced product density, not cockpit-packed)

On this page