A component library is the practical output of a design system. Atoms combine into molecules, molecules into organisms, organisms into pages. The craft is in the API: how the component exposes its props, handles composition, documents itself, and fails gracefully when used wrong.
A component is a reusable piece of UI with a defined API — its props, its slots, its events. Atomic design gives you a vocabulary for scale: atoms (button, input, badge), molecules (search bar = input + button), organisms (header = logo + nav + search bar), templates (page layout with placeholder content), and pages (templates filled with real data).
The value is compounding. Every time you use a component instead of writing one-off markup, you get consistency, accessibility, and testability for free. Every time you copy-paste a component and tweak it, you create a maintenance debt that compounds just as fast in the other direction.
// Button.stories.tsx — Storybook documentation import type { Meta, StoryObj } from '@storybook/react' import { Button } from './Button' const meta: Meta<typeof Button> = { component: Button, tags: ['autodocs'], argTypes: { variant: { control: 'select', options: ['primary', 'secondary', 'ghost'] }, size: { control: 'select', options: ['sm', 'md', 'lg'] }, }, } export default meta export const Primary: StoryObj = { args: { variant: 'primary', children: 'Deploy' }, } export const Ghost: StoryObj = { args: { variant: 'ghost', children: 'Cancel' }, }
// Component with variant pattern via cva (class-variance-authority) import { cva, type VariantProps } from 'class-variance-authority' const buttonVariants = cva( // Base classes shared across all variants 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:opacity-50', { variants: { variant: { primary: 'bg-blue text-paper hover:bg-blue-glow', secondary: 'bg-navy text-paper border border-border hover:bg-bg-card', ghost: 'bg-transparent text-secondary hover:bg-bg-card', }, size: { sm: 'h-8 px-3 text-xs', md: 'h-10 px-4 text-sm', lg: 'h-12 px-6 text-base', }, }, defaultVariants: { variant: 'primary', size: 'md' }, } ) interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {} export function Button({ variant, size, className, ...props }: ButtonProps) { return <button className={buttonVariants({ variant, size, className })} {...props} /> }
// Composition pattern — compound component for a Card function Card({ children, className }) { return <div className={`bg-card border rounded-lg p-6 ${className}`}>{children}</div> } Card.Header = ({ children }) => <div className="mb-4">{children}</div> Card.Title = ({ children }) => <h3 className="text-lg font-bold">{children}</h3> Card.Body = ({ children }) => <div className="text-sm text-secondary">{children}</div> // Usage — composable, readable, no prop drilling <Card> <Card.Header> <Card.Title>Deployment status</Card.Title> </Card.Header> <Card.Body>All services healthy.</Card.Body> </Card>
A Button with 18 props is not a good component — it's a configuration file. Split it. Use compound components, render props, or slots. If the prop list is growing, the component is doing too much.
Storybook shows you what a component looks like right now. Visual regression (Chromatic, Percy, Playwright screenshots) shows you what changed. Without the diff, you won't catch the subtle breakages.
Don't abstract on day one. Build three instances of a pattern in real features, then extract the component. Premature abstraction locks you into the wrong API before you know what the right one is.
Every styled component should accept a className prop and pass it through. If consumers can't override your styles, they'll work around your component instead of using it.