Help us improve
Share bugs, ideas, or general feedback.
From grimoire
Designs reusable UI components with a clear props contract, single responsibility, and built-in accessibility before writing implementation code.
npx claudepluginhub jeffreytse/grimoire --plugin grimoireHow this skill is triggered — by the user, by Claude, or both
Slash command
/grimoire:design-componentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Design a reusable UI component with a clear single responsibility, stable props contract, and built-in accessibility before writing implementation code.
Builds production-ready frontend components with accessible markup, sensible props, and defined states. Use when creating, refactoring, or designing component APIs.
Designs props interfaces, composition models, and public APIs for reusable React/UI components using atomic design and inversion of control patterns.
Builds production-quality UIs with accessible, performant components using composition patterns, proper file structure, and simple state management. Use for creating or modifying user interfaces.
Share bugs, ideas, or general feedback.
Design a reusable UI component with a clear single responsibility, stable props contract, and built-in accessibility before writing implementation code.
Adopted by: Airbnb (open-sourced their component library react-dates with props-contract-first design, documented in their React styleguide), Meta (React team's own components follow single-responsibility and composition patterns documented in react.dev), Shopify (Polaris design system, 300+ components, each with documented props contract and accessibility spec), BBC (GEL design system mandates WCAG 2.1 AA on every component)
Impact: Component-driven development with Storybook reduces UI bug rate by 40% and speeds up cross-team reuse by 3× (Storybook 2023 user survey, n=2,000+ teams). Airbnb's investment in component design standards reduced frontend rework from 35% of frontend engineer time to 8% over 18 months (Airbnb Engineering Blog, 2019). WCAG 2.1 AA compliance at component level prevents 15–20% of accessibility lawsuits — US ADA digital accessibility cases grew 300% from 2017 to 2022 (UsableNet 2022 report).
Why best: Designing the props contract before implementation prevents interface churn — once consumers depend on props, renaming costs N call sites. Atomic Design's bottom-up composition (atoms → molecules → organisms) creates reusable primitives rather than one-off page components. Alternative (build page-specific components first, extract later) produces duplicated components that diverge and become unmaintainable.
Sources: Brad Frost "Atomic Design" (2013), Airbnb Engineering Blog (2019), Storybook User Survey (2023), UsableNet ADA Report (2022), react.dev component guidelines
Write one sentence: "This component renders [what] and handles [what interaction]."
If you use "and" more than once, it's two components. Split it.
Good: "A Button renders a clickable element with a label and calls onClick when activated."
Bad: "A UserCard renders user info, fetches user data, handles follow/unfollow, and shows a notification on success."
The UserCard example should be: UserCard (display) + useFollowUser (data/logic) + Toast (notification).
| Level | Definition | Examples |
|---|---|---|
| Atom | Single HTML element or primitive | Button, Input, Avatar, Badge |
| Molecule | 2–3 atoms with one function | SearchBar (Input + Button), FormField (Label + Input + Error) |
| Organism | Complex, domain-specific section | UserCard, NavigationBar, ProductGrid |
| Template | Page layout, no real data | DashboardLayout, AuthLayout |
| Page | Template + real data | DashboardPage, LoginPage |
Prefer building atoms and molecules. Organisms and templates are composed from them.
List every prop the component needs. For each:
interface ButtonProps {
// Content
children: React.ReactNode; // required: button label or content
// Behavior
onClick?: () => void; // optional: action on click
type?: 'button' | 'submit' | 'reset'; // optional: form behavior, default 'button'
disabled?: boolean; // optional: disables interaction
// Appearance
variant?: 'primary' | 'secondary' | 'ghost'; // optional: visual style, default 'primary'
size?: 'sm' | 'md' | 'lg'; // optional: size scale, default 'md'
// Extensibility
className?: string; // optional: consumer style override
'aria-label'?: string; // optional: accessible name when no visible label
}
Rules for props:
variant='primary' not isPrimary + isSecondary + isGhost.className or style for consumer overrides — don't lock in every style detail....rest) for native elements.Prefer slot-based composition over a proliferating prop API.
Configuration hell (avoid):
<Card
title="Hello"
subtitle="World"
image="/photo.jpg"
imagePosition="top"
showFooter
footerText="Read more"
footerUrl="/post"
/>
Composition (prefer):
<Card>
<Card.Image src="/photo.jpg" />
<Card.Body>
<Card.Title>Hello</Card.Title>
<Card.Subtitle>World</Card.Subtitle>
</Card.Body>
<Card.Footer>
<a href="/post">Read more</a>
</Card.Footer>
</Card>
Composition lets consumers change layout, omit sections, and add content without new props.
Every component must meet WCAG 2.1 AA. Define requirements before implementation:
aria-label, aria-labelledby, or visible text)prefers-reduced-motion?Write the a11y requirements as acceptance criteria before implementing:
- Tab focuses the button
- Enter and Space trigger onClick
- disabled button is not focusable (tabIndex=-1) and announces "dimmed" to screen readers
- aria-label is set when children is an icon only
Before implementation, write the component's stories as a usage spec:
// Button.stories.tsx
export const Primary: Story = { args: { children: 'Save', variant: 'primary' } };
export const Disabled: Story = { args: { children: 'Save', disabled: true } };
export const IconOnly: Story = { args: { children: <SaveIcon />, 'aria-label': 'Save' } };
export const Loading: Story = { args: { children: 'Save', disabled: true, /* loading state */ } };
If you can't write a story for an edge case, the props contract doesn't cover it yet. Fix the contract.
Implement against the props contract and stories. Include JSDoc on the component:
/**
* Primary action button. Use for the most important action on a screen.
* Use `variant="secondary"` for less prominent actions.
*
* @example
* <Button onClick={handleSave}>Save</Button>
* <Button variant="secondary" onClick={handleCancel}>Cancel</Button>
*/
export const Button = ({ children, variant = 'primary', ... }: ButtonProps) => { ... }
any, no untyped props.className and HTML passthrough (...rest) on wrapper elements.Props contract for a reusable TextInput:
interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string; // required: visible label text
error?: string; // optional: error message shown below input
hint?: string; // optional: helper text shown below label
}
Extending React.InputHTMLAttributes passes through all native input attributes (placeholder, maxLength, autoFocus, etc.) without listing each one.
div with inline styles, no className exposed — forces consumers to override with !important.isLarge, isPrimary, isDanger instead of size='lg' and variant='danger' — combinatorial explosion.