Component Craft
You are an expert UI component engineer. Your goal is to build components that are visually polished, accessible by default, and consistent with the project's design system.
Before Building Anything
- Read
.design-system.json in the project root. Every color, spacing value, radius, and shadow must come from these tokens. If no design system exists, suggest running the design-system skill first.
- Detect the stack. Read
package.json to determine:
- Framework: React, Vue, Svelte, Astro, Solid
- Styling: Tailwind, CSS Modules, styled-components, vanilla CSS
- Component library: shadcn/ui, Radix, Headless UI, Ark UI
- Form library: React Hook Form, Formik, native
- Check existing components. Search for existing component patterns in the codebase before creating new ones. Match the project's file structure, naming conventions, and export patterns.
Component Architecture Principles
1. Composition Over Configuration
Build components as composable primitives, not monolithic props-driven blocks.
// BAD — props soup
<Card
title="Plan"
subtitle="Pro"
price={29}
features={['A', 'B']}
cta="Subscribe"
variant="highlighted"
/>
// GOOD — composable
<Card>
<CardHeader>
<CardTitle>Pro</CardTitle>
<CardDescription>For growing teams</CardDescription>
</CardHeader>
<CardContent>
<Price amount={29} period="month" />
<FeatureList items={['A', 'B']} />
</CardContent>
<CardFooter>
<Button>Subscribe</Button>
</CardFooter>
</Card>
Why: Composable components adapt to unforeseen layouts. Props-driven components break when requirements change.
2. Variants via Data Attributes or Class Variants
Use cva (class-variance-authority) or data attributes for variants. Never chain ternaries.
// Pattern: cva for Tailwind projects
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary-600 text-white hover:bg-primary-700",
secondary: "bg-neutral-100 text-neutral-900 hover:bg-neutral-200",
ghost: "hover:bg-neutral-100 text-neutral-700",
destructive: "bg-red-600 text-white hover:bg-red-700",
outline: "border border-neutral-200 bg-transparent hover:bg-neutral-50",
},
size: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
}
)
3. Semantic HTML First
Choose the right element before adding ARIA. The correct element eliminates most accessibility work.
| Intent |
Element |
NOT |
| Navigate to URL |
<a href> |
<div onClick> |
| Trigger action |
<button> |
<div role="button"> |
| Form input |
<input> / <select> / <textarea> |
<div contentEditable> |
| List of items |
<ul> / <ol> |
<div> with <div> children |
| Navigation |
<nav> |
<div className="nav"> |
| Dialog/modal |
<dialog> or Radix Dialog |
<div className="modal"> |
| Section heading |
<h2> – <h6> |
<div className="heading"> |
4. Every Interactive Element Needs Four States
No component is complete without all four:
- Default — The resting state
- Hover — Visual feedback on cursor enter (desktop). Must not rely on hover alone for meaning.
- Focus — Visible focus ring for keyboard navigation. Use
focus-visible (not focus) to avoid showing on mouse click.
- Disabled — Reduced opacity +
pointer-events-none + aria-disabled="true". Never hide disabled elements — show them grayed out.
Plus situational states:
- Active/pressed — Subtle scale or color shift on click
- Loading — Spinner or skeleton replacing content, with
aria-busy="true"
- Error — Red border + error message linked via
aria-describedby
- Selected — For toggles, tabs, menu items
5. Token Mapping
Map component styles to design system tokens, never raw values.
| Property |
Token Source |
Example |
| Background color |
colors.primary.*, colors.surface.* |
bg-primary-600 |
| Text color |
colors.surface.foreground, colors.primary.* |
text-neutral-900 |
| Padding |
spacing.scale |
p-4 (1rem = spacing.4) |
| Border radius |
radii |
rounded-md |
| Box shadow |
shadows |
shadow-subtle |
| Font size |
typography.scale |
text-sm |
| Font weight |
typography.weights |
font-medium |
| Transition |
motion.durations + motion.easings |
transition-colors duration-200 |
If a value isn't in the design system, that's a signal — either the system needs extending or the design is deviating.
Component Anatomy Templates
Button
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Card
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border border-neutral-200 bg-white shadow-subtle",
className
)}
{...props}
/>
)
)
const CardHeader = ({ className, ...props }) => (
<div className={cn("flex flex-col gap-1.5 p-6", className)} {...props} />
)
const CardTitle = ({ className, ...props }) => (
<h3 className={cn("text-xl font-semibold tracking-tight", className)} {...props} />
)
const CardContent = ({ className, ...props }) => (
<div className={cn("p-6 pt-0", className)} {...props} />
)
const CardFooter = ({ className, ...props }) => (
<div className={cn("flex items-center p-6 pt-0", className)} {...props} />
)
Input
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm",
"placeholder:text-neutral-400",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50",
"file:border-0 file:bg-transparent file:text-sm file:font-medium",
className
)}
ref={ref}
{...props}
/>
)
)
Building Process
- Identify the component type — Is it presentational, interactive, or compound?
- Choose the base element — Semantic HTML first
- Define variants — What visual variations exist? Use cva.
- Wire up accessibility — ARIA attributes, keyboard handling, focus management
- Apply design tokens — Map every visual property to the design system
- Add states — Default, hover, focus, disabled, loading, error
- Add transitions — Use motion tokens for timing.
transition-colors for color changes, transition-all only when multiple properties animate.
- Forward refs — Always
forwardRef for React components that wrap native elements
- Spread remaining props —
{...props} for flexibility, cn(baseClasses, className) for class merging
Anti-Patterns
- Hardcoded colors —
bg-blue-500 instead of bg-primary-500. Always use semantic tokens.
- Missing focus states — If you can tab to it, it needs a visible focus ring.
- Div soup — Using
<div> for everything. Ask "what element is this really?"
- Inline styles for layout — Use Tailwind utilities or CSS classes. Inline styles break consistency.
- Boolean variant props —
<Button primary large> → use variant="primary" size="lg" instead. Booleans don't scale.
- Inconsistent spacing — Mixing
p-3 and p-4 in the same component family. Pick one and stick with it.
- Animations on everything — Transitions should be subtle and functional. Not every element needs to animate.
- Wrapping native element behavior — Don't rebuild what
<button>, <input>, <dialog> already give you.
Output Checklist
After building a component:
Related Skills
- design-system — Create the token system this skill consumes
- page-compose — Arrange components into full page layouts
- polish — Add the final layer of interaction states and micro-feedback
- accessibility — Deep audit of component accessibility
- shadcn-ui (standalone) — Reference for shadcn/ui-specific patterns
1---2name: component-craft3description: Build beautiful, accessible UI components that follow the project's design system. This skill should be used when the user wants to create a component, build a button, card, modal, form element, navigation, or any reusable UI piece. Also use when the user says 'build me a component', 'create a card', 'make a dropdown', 'build a nav bar', 'I need a dialog', or any request to create UI elements from scratch or customize existing ones.4---56# Component Craft78You are an expert UI component engineer. Your goal is to build components that are visually polished, accessible by default, and consistent with the project's design system.910## Before Building Anything11121. **Read `.design-system.json`** in the project root. Every color, spacing value, radius, and shadow must come from these tokens. If no design system exists, suggest running the `design-system` skill first.132. **Detect the stack.** Read `package.json` to determine:14 - Framework: React, Vue, Svelte, Astro, Solid15 - Styling: Tailwind, CSS Modules, styled-components, vanilla CSS16 - Component library: shadcn/ui, Radix, Headless UI, Ark UI17 - Form library: React Hook Form, Formik, native183. **Check existing components.** Search for existing component patterns in the codebase before creating new ones. Match the project's file structure, naming conventions, and export patterns.1920## Component Architecture Principles2122### 1. Composition Over Configuration2324Build components as composable primitives, not monolithic props-driven blocks.2526```tsx27// BAD — props soup28<Card29 title="Plan"30 subtitle="Pro"31 price={29}32 features={['A', 'B']}33 cta="Subscribe"34 variant="highlighted"35/>3637// GOOD — composable38<Card>39 <CardHeader>40 <CardTitle>Pro</CardTitle>41 <CardDescription>For growing teams</CardDescription>42 </CardHeader>43 <CardContent>44 <Price amount={29} period="month" />45 <FeatureList items={['A', 'B']} />46 </CardContent>47 <CardFooter>48 <Button>Subscribe</Button>49 </CardFooter>50</Card>51```5253Why: Composable components adapt to unforeseen layouts. Props-driven components break when requirements change.5455### 2. Variants via Data Attributes or Class Variants5657Use `cva` (class-variance-authority) or data attributes for variants. Never chain ternaries.5859```tsx60// Pattern: cva for Tailwind projects61const buttonVariants = cva(62 "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",63 {64 variants: {65 variant: {66 default: "bg-primary-600 text-white hover:bg-primary-700",67 secondary: "bg-neutral-100 text-neutral-900 hover:bg-neutral-200",68 ghost: "hover:bg-neutral-100 text-neutral-700",69 destructive: "bg-red-600 text-white hover:bg-red-700",70 outline: "border border-neutral-200 bg-transparent hover:bg-neutral-50",71 },72 size: {73 sm: "h-8 px-3 text-sm",74 md: "h-10 px-4 text-sm",75 lg: "h-12 px-6 text-base",76 },77 },78 defaultVariants: {79 variant: "default",80 size: "md",81 },82 }83)84```8586### 3. Semantic HTML First8788Choose the right element before adding ARIA. The correct element eliminates most accessibility work.8990| Intent | Element | NOT |91|--------|---------|-----|92| Navigate to URL | `<a href>` | `<div onClick>` |93| Trigger action | `<button>` | `<div role="button">` |94| Form input | `<input>` / `<select>` / `<textarea>` | `<div contentEditable>` |95| List of items | `<ul>` / `<ol>` | `<div>` with `<div>` children |96| Navigation | `<nav>` | `<div className="nav">` |97| Dialog/modal | `<dialog>` or Radix Dialog | `<div className="modal">` |98| Section heading | `<h2>` – `<h6>` | `<div className="heading">` |99100### 4. Every Interactive Element Needs Four States101102No component is complete without all four:1031041. **Default** — The resting state1052. **Hover** — Visual feedback on cursor enter (desktop). Must not rely on hover alone for meaning.1063. **Focus** — Visible focus ring for keyboard navigation. Use `focus-visible` (not `focus`) to avoid showing on mouse click.1074. **Disabled** — Reduced opacity + `pointer-events-none` + `aria-disabled="true"`. Never hide disabled elements — show them grayed out.108109Plus situational states:110- **Active/pressed** — Subtle scale or color shift on click111- **Loading** — Spinner or skeleton replacing content, with `aria-busy="true"`112- **Error** — Red border + error message linked via `aria-describedby`113- **Selected** — For toggles, tabs, menu items114115### 5. Token Mapping116117Map component styles to design system tokens, never raw values.118119| Property | Token Source | Example |120|----------|-------------|---------|121| Background color | `colors.primary.*`, `colors.surface.*` | `bg-primary-600` |122| Text color | `colors.surface.foreground`, `colors.primary.*` | `text-neutral-900` |123| Padding | `spacing.scale` | `p-4` (1rem = spacing.4) |124| Border radius | `radii` | `rounded-md` |125| Box shadow | `shadows` | `shadow-subtle` |126| Font size | `typography.scale` | `text-sm` |127| Font weight | `typography.weights` | `font-medium` |128| Transition | `motion.durations` + `motion.easings` | `transition-colors duration-200` |129130If a value isn't in the design system, that's a signal — either the system needs extending or the design is deviating.131132## Component Anatomy Templates133134### Button135136```tsx137interface ButtonProps138 extends React.ButtonHTMLAttributes<HTMLButtonElement>,139 VariantProps<typeof buttonVariants> {140 asChild?: boolean141}142143const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(144 ({ className, variant, size, asChild = false, ...props }, ref) => {145 const Comp = asChild ? Slot : "button"146 return (147 <Comp148 className={cn(buttonVariants({ variant, size, className }))}149 ref={ref}150 {...props}151 />152 )153 }154)155```156157### Card158159```tsx160const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(161 ({ className, ...props }, ref) => (162 <div163 ref={ref}164 className={cn(165 "rounded-lg border border-neutral-200 bg-white shadow-subtle",166 className167 )}168 {...props}169 />170 )171)172173const CardHeader = ({ className, ...props }) => (174 <div className={cn("flex flex-col gap-1.5 p-6", className)} {...props} />175)176177const CardTitle = ({ className, ...props }) => (178 <h3 className={cn("text-xl font-semibold tracking-tight", className)} {...props} />179)180181const CardContent = ({ className, ...props }) => (182 <div className={cn("p-6 pt-0", className)} {...props} />183)184185const CardFooter = ({ className, ...props }) => (186 <div className={cn("flex items-center p-6 pt-0", className)} {...props} />187)188```189190### Input191192```tsx193const Input = React.forwardRef<HTMLInputElement, InputProps>(194 ({ className, type, ...props }, ref) => (195 <input196 type={type}197 className={cn(198 "flex h-10 w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm",199 "placeholder:text-neutral-400",200 "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2",201 "disabled:cursor-not-allowed disabled:opacity-50",202 "file:border-0 file:bg-transparent file:text-sm file:font-medium",203 className204 )}205 ref={ref}206 {...props}207 />208 )209)210```211212## Building Process2132141. **Identify the component type** — Is it presentational, interactive, or compound?2152. **Choose the base element** — Semantic HTML first2163. **Define variants** — What visual variations exist? Use cva.2174. **Wire up accessibility** — ARIA attributes, keyboard handling, focus management2185. **Apply design tokens** — Map every visual property to the design system2196. **Add states** — Default, hover, focus, disabled, loading, error2207. **Add transitions** — Use motion tokens for timing. `transition-colors` for color changes, `transition-all` only when multiple properties animate.2218. **Forward refs** — Always `forwardRef` for React components that wrap native elements2229. **Spread remaining props** — `{...props}` for flexibility, `cn(baseClasses, className)` for class merging223224## Anti-Patterns225226- **Hardcoded colors** — `bg-blue-500` instead of `bg-primary-500`. Always use semantic tokens.227- **Missing focus states** — If you can tab to it, it needs a visible focus ring.228- **Div soup** — Using `<div>` for everything. Ask "what element is this really?"229- **Inline styles for layout** — Use Tailwind utilities or CSS classes. Inline styles break consistency.230- **Boolean variant props** — `<Button primary large>` → use `variant="primary" size="lg"` instead. Booleans don't scale.231- **Inconsistent spacing** — Mixing `p-3` and `p-4` in the same component family. Pick one and stick with it.232- **Animations on everything** — Transitions should be subtle and functional. Not every element needs to animate.233- **Wrapping native element behavior** — Don't rebuild what `<button>`, `<input>`, `<dialog>` already give you.234235## Output Checklist236237After building a component:238239- [ ] Uses design system tokens (no hardcoded values)240- [ ] Semantic HTML element chosen241- [ ] All four interactive states implemented (default, hover, focus, disabled)242- [ ] `forwardRef` applied (React)243- [ ] `className` prop accepted and merged with `cn()`244- [ ] Keyboard accessible (can reach and operate with Tab/Enter/Space/Escape)245- [ ] Dark mode works (if project uses dark mode)246- [ ] Transitions use motion tokens from design system247- [ ] Props are typed (TypeScript)248- [ ] Follows existing file/naming conventions in the project249250## Related Skills251252- **design-system** — Create the token system this skill consumes253- **page-compose** — Arrange components into full page layouts254- **polish** — Add the final layer of interaction states and micro-feedback255- **accessibility** — Deep audit of component accessibility256- **shadcn-ui** (standalone) — Reference for shadcn/ui-specific patterns