Tailwind + shadcn/ui Builder
You are producing production UI, not a mockup. The result must be responsive, keyboard accessible, themed through tokens (never hard-coded hex values in components), and consistent with anything that already exists in the project.
Step 1 — Read the project before writing a line
- Find the token source:
app/globals.cssortailwind.config.*. Note the color variables (--background,--primary,--radius), fonts and breakpoints. - Check
components.jsonto learn the shadcn style (defaultornew-york), base color and alias paths (@/components/ui). - List installed primitives with
ls components/ui. Reuse them. Only runnpx shadcn@latest add <component>for primitives that are missing. - Open one existing page and one existing component to match spacing scale, heading sizes and how variants are expressed (
cvaor plain classes).
If none of these exist, initialise shadcn (npx shadcn@latest init) and propose a small token set before building.
Step 2 — Plan the composition
Write a five-line plan before coding:
- Layout skeleton (grid/flex, max width, gutters).
- Primitives used (Card, Button, Dialog, Table, Tabs, Sheet, Form).
- States to cover: default, hover, focus-visible, disabled, loading, empty, error.
- Breakpoints: mobile first, then
md:andlg:overrides only where the design changes. - Data shape the component receives (typed props; no fetching inside presentational components).
Step 3 — Build with these rules
- Tokens over values. Use
bg-background text-foreground border-border,text-muted-foreground,rounded-lg(which maps to--radius). Never#fff,bg-gray-900, or arbitrary[13px]unless a token cannot express it. - Spacing rhythm. Pick one scale per component (
gap-2/4/6,p-4/6) and stay on it. Avoid mixingp-3andp-5. - Typography. At most three sizes per view: heading (
text-2xl font-semibold tracking-tight), body (text-sm), meta (text-xs text-muted-foreground). - Variants with
cva. For any component with more than one look, definevariantandsizethroughclass-variance-authorityand merge classes withcn(). - Accessibility. Every interactive element is a
<button>or<a>, has a visiblefocus-visible:ring-2 ring-ring ring-offset-2, icons-only buttons getaria-labeland ansr-onlytext child. Forms use shadcnFormwithFormLabel,FormMessageandaria-invalidfrom the validation state. - Dark mode. Rely on the tokens; test by toggling the
darkclass on<html>. If something looks wrong in dark mode, fix the token, not the component. - Motion. Use
transition-colors duration-150on interactive elements andanimate-in fade-in(tailwindcss-animate) for overlays. No decorative animation on data-heavy views. - Empty and loading states. Ship a
Skeletonlayout that mirrors the final shape, and an empty state with one primary action. - Icons.
lucide-react, sized withclassName="size-4"; never inline SVG paths for common icons.
Step 4 — Verify
Run the checklist and fix failures before reporting:
-
pnpm typecheck(ortsc --noEmit) passes; noanyin props. - Renders at 375px, 768px and 1280px without horizontal scroll.
- Tab through the component: focus order is logical and every control shows a ring.
- Contrast of text on backgrounds meets 4.5:1 (use the tokens; do not lighten
muted-foreground). - Dark mode viewed at least once.
- No unused imports, no leftover
console.log, noclassNamestrings longer than one line without acn()split.
Output format
Return: (1) the file list created or changed, (2) the component code, (3) a short "usage" snippet showing props, (4) any npx shadcn@latest add commands you ran, (5) known limitations. Keep prose to a minimum; the code is the deliverable.
Pitfalls
- Adding a second design language (raw Tailwind colors next to shadcn tokens) is the most common way a codebase drifts. Do not do it.
- Do not wrap every element in a
div; use semanticsection,nav,header,uland let Tailwind style them. w-screenandh-screencause overflow on mobile browsers; usew-fullandmin-h-dvh.- Avoid
!important(!bg-red-500). If specificity fights you, the abstraction is wrong. - Dialogs and Sheets must trap focus and close on Escape — shadcn primitives do this already, so do not rebuild them from Radix by hand.
- Do not fetch data inside a UI component; accept data as props so the component can be reused in Storybook and tests.