The complete vibe coding loop — describe, generate, review, refine
Every component you build with Claude follows the same 5-step loop. Master this loop and you can build anything:
A good prompt has these parts:
Create a [COMPONENT NAME] component at [FILE PATH].
Purpose: [What does this component do?]
Variants: [List all visual variants]
Props: [List all props with types]
States: [Hover, focus, disabled, loading, error, etc.]
Content: [What goes inside? Text, icons, children?]
Behavior: [Click handlers, animations, transitions]
Constraints: [Tailwind only, no inline styles, follow SKILL.md, etc.]Create a Card component at src/components/ui/Card.tsx.
Purpose: A container for grouping related content with optional header and footer.
Variants: default (white bg, subtle border), elevated (white bg, shadow-md), outlined (transparent bg, strong border)
Props:
- variant: "default" | "elevated" | "outlined" (default: "default")
- padding: "sm" | "md" | "lg" (default: "md")
- children: ReactNode
- className?: string (for overrides)
States: Hover — elevated variant gets shadow-lg on hover
Content: Children slot for any content
Behavior: No click handlers — purely presentational
Constraints: Tailwind only, use design tokens from SKILL.md, TypeScript strictEvery design system starts with three foundational components. Build these first to establish your patterns, then everything else follows the same approach.
Create a Button component at src/components/ui/Button.tsx.
Purpose: The primary interactive element across the application.
Variants:
- primary: dark background (#1A1A1A), white text
- secondary: white background, dark border, dark text
- ghost: transparent background, no border, dark text
- danger: red background (#DC2626), white text
Sizes: sm (h-8, text-sm, px-3), md (h-10, text-sm, px-4), lg (h-12, text-base, px-6)
Props:
- label: string
- variant: "primary" | "secondary" | "ghost" | "danger" (default: "primary")
- size: "sm" | "md" | "lg" (default: "md")
- onClick?: () => void
- disabled?: boolean
- loading?: boolean
- icon?: ReactNode (optional, renders before label)
States:
- Hover: slight opacity change or color shift
- Focus: ring-2 ring-offset-2 ring-primary
- Disabled: opacity-50, cursor-not-allowed, pointer-events-none
- Loading: show a spinner, disable clicks
Constraints: Tailwind only, use cn() for conditional classes, TypeScript strict, default exportCreate an Input component at src/components/ui/Input.tsx.
Purpose: Text input field with label, helper text, and error states.
Props:
- label: string
- placeholder?: string
- value: string
- onChange: (value: string) => void
- type: "text" | "email" | "password" | "number" (default: "text")
- error?: string (shows error message below input)
- helperText?: string (shows helper text below input, hidden when error is present)
- disabled?: boolean
- required?: boolean
States:
- Default: gray border
- Focus: ring-2 ring-primary, border-primary
- Error: red border, red ring, error text in red below
- Disabled: bg-gray-50, opacity-50, cursor-not-allowed
Layout:
- Label above the input
- Input field full width
- Helper text or error text below
Constraints: Tailwind only, TypeScript strict, default export, use forwardRefCreate a Card component at src/components/ui/Card.tsx.
Purpose: Content container with optional header, body, and footer sections.
Variants:
- default: white bg, border border-gray-200, rounded-xl
- elevated: white bg, shadow-md, rounded-xl
- outlined: transparent bg, border-2 border-gray-300, rounded-xl
Props:
- variant: "default" | "elevated" | "outlined" (default: "default")
- padding: "none" | "sm" | "md" | "lg" (default: "md")
- children: ReactNode
- className?: string
Sub-components (compound pattern):
- Card.Header — top section with bottom border
- Card.Body — main content area
- Card.Footer — bottom section with top border
Constraints: Tailwind only, TypeScript strict, use cn() utilityWhen Claude generates a component, run through this checklist before accepting it:
any).cn() for conditionals.When something isn’t right, be specific about what needs to change. Vague refinement prompts lead to vague results.
"Make it look better"
"Fix the styling"
"It doesn't look right"
"Can you improve it?""The hover state on the primary variant should change the background to #333333, not opacity"
"The disabled state is missing cursor-not-allowed — add it to all variants"
"The padding on the sm size is too tight — change from px-2 to px-3"
"Add a focus-visible ring using ring-2 ring-offset-2 ring-blue-500 to all interactive variants"
"The error state border should be red-500 not red-600, and the error text should be text-sm not text-xs"Don’t ask Claude to fix 5 things at once. Fix one thing, verify it’s correct, then move to the next. This gives you more control and makes it easier to catch if Claude introduces a new bug while fixing the first one.
Tip: If Claude keeps getting something wrong after 2-3 attempts, try rephrasing your prompt completely. Sometimes a different description of the same thing clicks better.
Once you have your foundational components, you can compose them into larger, more complex components. These are called compound components or pattern components.
Create a LoginForm component at src/components/patterns/LoginForm.tsx.
Purpose: A complete login form with email, password, and submit button.
Compose these existing components:
- Input (from src/components/ui/Input.tsx)
- Button (from src/components/ui/Button.tsx)
- Card (from src/components/ui/Card.tsx)
Layout:
- Wrapped in a Card (elevated variant, md padding)
- Card.Header with "Sign In" title and "Welcome back" subtitle
- Card.Body with:
- Email Input (type="email", required)
- Password Input (type="password", required)
- "Forgot password?" link aligned right
- Card.Footer with:
- Primary Button (label="Sign In", full width)
- Text: "Don't have an account? Sign up" with link
Behavior:
- Form validates on submit
- Show error states on invalid inputs
- Button shows loading state during submission
- onSubmit prop: (email: string, password: string) => Promise<void>
Constraints: Import existing components, don't recreate them. Tailwind only. TypeScript strict.Tip: Always tell Claude which existing components to import. If you don’t, it will create everything from scratch and you’ll end up with duplicated code that doesn’t match your design system.