Frontend Architecture
Part 1 of 11Designing a Component Library That Teams Actually Adopt
Designing a Component Library That Teams Actually Adopt
Introduction
Every company with more than one frontend team eventually decides they need a component library. The pitch is compelling: consistent UI, faster development, single source of truth, reduced duplication.
Then reality hits.
Six months later, the library exists but adoption is patchy. Some teams use it. Others don't—they say it's "too opinionated" or "doesn't fit their use case." The components that do get used are constantly forked. The library team is underwater handling requests while also being criticized for not shipping fast enough.
The problem usually isn't technical. The components work. The Storybook looks great. The TypeScript types are thorough.
The problem is that a component library is a product, and the developers who should use it are your users. Building components is the easy part. Building components that teams actually adopt requires treating adoption as a design constraint from day one.
This guide covers how to design a component library for adoption, not just correctness.
Why Component Libraries Fail to Get Adopted
The Adoption Gap
THE COMPONENT LIBRARY PARADOX:
════════════════════════════════════════════════════════════════════
What library teams build:
┌─────────────────────────────────────────────────────────────┐
│ • Comprehensive component set │
│ • Pixel-perfect design system implementation │
│ • Thorough TypeScript types │
│ • Extensive prop options for flexibility │
│ • Sophisticated theming system │
│ • Carefully considered accessibility │
└─────────────────────────────────────────────────────────────┘
What teams actually need:
┌─────────────────────────────────────────────────────────────┐
│ • A button that works like they expect │
│ • Copy-paste examples for common patterns │
│ • Answers when they're stuck │
│ • Components that don't fight their code │
│ • Updates that don't break their app │
│ • The thing design gave them, buildable in an hour │
└─────────────────────────────────────────────────────────────┘
Gap: The library is built for completeness.
Teams need ease.
The Real Reasons Teams Don't Adopt
┌─────────────────────────────────────────────────────────────────────┐
│ WHY TEAMS REJECT COMPONENT LIBRARIES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ "IT'S TOO HARD TO USE" │
│ ───────────────────────────────────────────────────────────────── │
│ • Too many props to understand │
│ • No copy-paste examples │
│ • Documentation assumes expertise │
│ • Simple things require complex setup │
│ │
│ Reality: Teams will write their own if yours takes longer. │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ "IT DOESN'T DO WHAT I NEED" │
│ ───────────────────────────────────────────────────────────────── │
│ • Missing component that design requires │
│ • Existing component can't do the specific thing │
│ • Styling/theming doesn't match their context │
│ • Edge case isn't supported │
│ │
│ Reality: You can't anticipate every need. Make extension easy. │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ "IT KEEPS BREAKING MY APP" │
│ ───────────────────────────────────────────────────────────────── │
│ • Upgrades cause unexpected changes │
│ • Dependencies conflict with their stack │
│ • Bundle size impact │
│ • Performance issues │
│ │
│ Reality: Trust is lost with every breaking change. │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ "NOBODY RESPONDS WHEN I NEED HELP" │
│ ───────────────────────────────────────────────────────────────── │
│ • Issues sit open for weeks │
│ • No one answers Slack questions │
│ • Feature requests go into a void │
│ • Feels like a side project, not a supported product │
│ │
│ Reality: A library without support is abandonware. │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ "WE ALREADY HAVE SOMETHING THAT WORKS" │
│ ───────────────────────────────────────────────────────────────── │
│ • Migration cost is high │
│ • Current solution is "good enough" │
│ • No clear benefit to switching │
│ • Team has more pressing priorities │
│ │
│ Reality: You're competing with the status quo. You must be │
│ significantly better, not just different. │
│ │
└─────────────────────────────────────────────────────────────────────┘
Principles for Adoptable Libraries
The Adoption-First Mindset
CORE PRINCIPLE:
════════════════════════════════════════════════════════════════════
ADOPTION IS A FEATURE.
A technically perfect component that no one uses is worse than
a "good enough" component that everyone uses.
Every design decision should be evaluated against:
"Will this make teams more or less likely to adopt?"
THE ADOPTION HIERARCHY:
════════════════════════════════════════════════════════════════════
┌─────────────────┐
│ DELIGHTFUL │ ← Teams evangelize it
├─────────────────┤
│ POWERFUL │ ← Handles complex cases
├─────────────────┤
│ EXTENSIBLE │ ← Teams can customize
├─────────────────┤
│ PREDICTABLE │ ← No surprises
├─────────────────┤
│ USABLE │ ← Teams can figure it out
├─────────────────┤
│ FUNCTIONAL │ ← It works at all
└─────────────────┘
Start at the bottom. Each level must be solid before the next
matters. A delightful component that's unpredictable won't be
adopted.
The Seven Principles
┌─────────────────────────────────────────────────────────────────────┐
│ PRINCIPLES FOR ADOPTABLE LIBRARIES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. MAKE THE SIMPLE THINGS SIMPLE │
│ ───────────────────────────────────────────────────────────────── │
│ 80% of use cases should require minimal effort. │
│ Don't make users pay for features they don't use. │
│ │
│ <Button>Click me</Button> ✓ Just works │
│ <Button variant="primary" size="md" colorScheme="blue" │
│ fontWeight="semibold" >Click me</Button> ✗ Overwhelming │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ 2. MAKE THE COMPLEX THINGS POSSIBLE │
│ ───────────────────────────────────────────────────────────────── │
│ Don't sacrifice flexibility for simplicity. Provide escape │
│ hatches for advanced use cases. │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ 3. BE PREDICTABLE │
│ ───────────────────────────────────────────────────────────────── │
│ Components should behave like developers expect. Surprises │
│ destroy trust and cause bugs. │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ 4. COMPOSITION OVER CONFIGURATION │
│ ───────────────────────────────────────────────────────────────── │
│ Smaller, composable pieces beat mega-components with 50 props. │
│ Let users build what they need from primitives. │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ 5. PLAY NICE WITH OTHERS │
│ ───────────────────────────────────────────────────────────────── │
│ Don't fight the ecosystem. Support standard patterns, don't │
│ require special setup, integrate with popular tools. │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ 6. DOCUMENT FOR ADOPTION, NOT COMPLETENESS │
│ ───────────────────────────────────────────────────────────────── │
│ Copy-paste examples > prop tables. Real patterns > edge cases. │
│ Answer "how do I...?" before "what does X do?" │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ 7. SUPPORT IS PART OF THE PRODUCT │
│ ───────────────────────────────────────────────────────────────── │
│ A library without responsive support is abandonware. │
│ Budget for answering questions, not just writing code. │
│ │
└─────────────────────────────────────────────────────────────────────┘
API Design for Adoption
The Simple-to-Advanced Spectrum
DESIGNING FOR THE SPECTRUM:
════════════════════════════════════════════════════════════════════
Level 1: BASIC (Most users, most of the time)
───────────────────────────────────────────────
Minimal props, sensible defaults, just works.
<Button onClick={handleClick}>Save</Button>
<Modal open={isOpen} onClose={handleClose}>
<p>Are you sure?</p>
</Modal>
<TextField
label="Email"
value={email}
onChange={setEmail}
/>
Level 2: CUSTOMIZED (Common variations)
───────────────────────────────────────
Props for common customization needs.
<Button variant="secondary" size="large">
Cancel
</Button>
<Modal size="large" closeOnOverlayClick={false}>
...
</Modal>
<TextField
label="Password"
type="password"
error={errors.password}
helperText="Must be 8+ characters"
/>
Level 3: COMPOSED (Building blocks)
───────────────────────────────────
Expose internal parts for custom composition.
<Modal.Root open={isOpen} onClose={handleClose}>
<Modal.Overlay blur />
<Modal.Content>
<Modal.Header>
<Modal.Title>Confirm</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>...</Modal.Body>
<Modal.Footer>
<Button onClick={handleClose}>Cancel</Button>
<Button variant="primary">Confirm</Button>
</Modal.Footer>
</Modal.Content>
</Modal.Root>
Level 4: CONTROLLED (Full control)
──────────────────────────────────
Expose refs, render props, or hooks for complete control.
const { isOpen, open, close, toggle } = useModal();
const triggerRef = useRef();
<Button ref={triggerRef} onClick={open}>Open</Button>
<Modal
open={isOpen}
onClose={close}
triggerRef={triggerRef}
{...customA11yProps}
>
...
</Modal>
PRINCIPLE: Users should be able to start at Level 1 and
gradually move to Level 4 as needs grow—without switching
to a different component.
Sensible Defaults
DEFAULTS THAT REDUCE FRICTION:
════════════════════════════════════════════════════════════════════
VISUAL DEFAULTS
───────────────
Component should look good out of the box.
Bad: Button with no styling, requires className
Good: Button looks like a button, styled appropriately
BEHAVIOR DEFAULTS
─────────────────
Component should behave as expected.
Bad: Modal doesn't close on Escape or overlay click
Good: Modal closes on Escape and overlay click by default
(with props to disable if needed)
ACCESSIBILITY DEFAULTS
──────────────────────
Component should be accessible without extra work.
Bad: User must add aria-label, role, keyboard handling
Good: Component handles ARIA, focus management, keyboard by default
INTEGRATION DEFAULTS
────────────────────
Component should work without special setup.
Bad: Requires wrapping app in Provider, importing CSS separately
Good: Works immediately after import
EXAMPLE: Button Defaults
────────────────────────
// What users write:
<Button onClick={handleSave}>Save</Button>
// What they get (via defaults):
<button
type="button" // Prevents accidental form submission
className="btn btn-primary" // Styled as primary by default
onClick={handleSave}
// Cursor, focus styles, hover states all handled
>
Save
</button>
Props API Design
PROP DESIGN GUIDELINES:
════════════════════════════════════════════════════════════════════
1. USE FAMILIAR PATTERNS
────────────────────────
Match React/DOM conventions. Don't invent new patterns.
Bad: <Input whenChanged={fn} />
Good: <Input onChange={fn} />
Bad: <Modal visible={true} />
Good: <Modal open={true} /> // or isOpen to match React state naming
2. BOOLEAN PROPS: POSITIVE FORM
───────────────────────────────
Name booleans for what they enable, not disable.
Bad: <Modal disableCloseOnOverlay />
Good: <Modal closeOnOverlayClick={false} />
Bad: <Input noAutoComplete />
Good: <Input autoComplete={false} />
3. LIMIT PROP COUNT
───────────────────
More than 10 commonly-used props = too complex.
Push complexity into composition or sub-components.
Bad:
<DataTable
data={data}
columns={columns}
sortable
sortColumn={sortCol}
sortDirection={sortDir}
onSort={handleSort}
filterable
filterValue={filter}
onFilter={handleFilter}
paginated
page={page}
pageSize={pageSize}
onPageChange={handlePage}
selectable
selectedRows={selected}
onSelect={handleSelect}
... 20 more props
/>
Good:
<DataTable data={data} columns={columns}>
<DataTable.Toolbar>
<DataTable.Search />
<DataTable.Filter />
</DataTable.Toolbar>
<DataTable.Pagination pageSize={20} />
<DataTable.Selection onSelect={handleSelect} />
</DataTable>
4. ENUM PROPS: EXPLICIT OPTIONS
───────────────────────────────
Use TypeScript literal types. Autocomplete guides users.
Bad:
<Button size="sm" /> // What are the options? Who knows.
Good:
type ButtonSize = 'small' | 'medium' | 'large';
<Button size="small" /> // IDE shows options
5. ESCAPE HATCHES
─────────────────
Allow passing arbitrary props/refs for edge cases.
// Spread remaining props to root element
const Button = ({ variant, size, children, ...rest }) => (
<button className={getClasses(variant, size)} {...rest}>
{children}
</button>
);
// Now users can add anything:
<Button data-testid="submit" aria-describedby="hint">
Save
</Button>
Composition Patterns
COMPOUND COMPONENTS:
════════════════════════════════════════════════════════════════════
Export related components together for flexible composition.
// The library exports:
export const Tabs = {
Root: TabsRoot,
List: TabsList,
Tab: Tab,
Panels: TabPanels,
Panel: TabPanel,
};
// Users can compose:
<Tabs.Root defaultValue="tab1">
<Tabs.List>
<Tabs.Tab value="tab1">Account</Tabs.Tab>
<Tabs.Tab value="tab2">Security</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel value="tab1">
<AccountSettings />
</Tabs.Panel>
<Tabs.Panel value="tab2">
<SecuritySettings />
</Tabs.Panel>
</Tabs.Panels>
</Tabs.Root>
// But also provide a simple version:
<Tabs
defaultValue="tab1"
items={[
{ value: 'tab1', label: 'Account', content: <AccountSettings /> },
{ value: 'tab2', label: 'Security', content: <SecuritySettings /> },
]}
/>
BENEFITS:
• Level 1 users use the simple version
• Level 3 users use compound components for custom layouts
• Same mental model scales from simple to complex
RENDER PROPS / SLOT PATTERN:
════════════════════════════════════════════════════════════════════
Allow custom rendering for specific parts.
// Default rendering:
<Autocomplete
options={countries}
getOptionLabel={(c) => c.name}
/>
// Custom rendering:
<Autocomplete
options={countries}
getOptionLabel={(c) => c.name}
renderOption={(country, { isHighlighted }) => (
<div className={isHighlighted ? 'highlighted' : ''}>
<Flag code={country.code} />
<span>{country.name}</span>
</div>
)}
/>
Users get default behavior OR complete control over rendering.
Documentation That Drives Adoption
Documentation Hierarchy
WHAT DEVELOPERS LOOK FOR (IN ORDER):
════════════════════════════════════════════════════════════════════
1. CAN I COPY-PASTE THIS? (5 seconds)
─────────────────────────────────────
"I need a button. Let me find an example I can copy."
If they can't copy-paste working code immediately, adoption drops.
2. HOW DO I DO THIS SPECIFIC THING? (30 seconds)
────────────────────────────────────────────────
"I need a loading button. Where's that example?"
If the common patterns aren't documented, they'll assume it's
not supported and build it themselves.
3. WHAT ARE MY OPTIONS? (2 minutes)
───────────────────────────────────
"What variants does this button have?"
Now they're looking at prop tables and examples.
4. HOW DOES THIS WORK? (5+ minutes)
───────────────────────────────────
"How does the focus management work in this modal?"
Only for complex components or curious developers.
DOCUMENTATION PRINCIPLE:
────────────────────────
Optimize for #1 and #2. Most developers never get to #4.
The Ideal Documentation Page
DOCUMENTATION PAGE STRUCTURE:
════════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────────┐
│ BUTTON │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Live preview of default button] [Copy code button] │
│ │
│ import { Button } from '@myorg/components'; │
│ │
│ <Button onClick={handleClick}>Click me</Button> │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ EXAMPLES │
│ ───────────────────────────────────────────────────────────── │
│ │
│ Variants │
│ [primary] [secondary] [outline] [ghost] [link] │
│ │
│ <Button variant="primary">Primary</Button> │
│ <Button variant="secondary">Secondary</Button> │
│ ... │
│ │
│ Sizes │
│ [small] [medium] [large] │
│ │
│ <Button size="small">Small</Button> │
│ ... │
│ │
│ With icon │
│ [icon + text example] │
│ │
│ <Button leftIcon={<PlusIcon />}>Add item</Button> │
│ │
│ Loading state │
│ [loading button example] │
│ │
│ <Button isLoading>Saving...</Button> │
│ │
│ Disabled │
│ [disabled example] │
│ │
│ As link │
│ [button-styled link] │
│ │
│ <Button as="a" href="/settings">Settings</Button> │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ COMMON PATTERNS │
│ ───────────────────────────────────────────────────────────── │
│ │
│ Form submit button with loading │
│ ──────────────────────────────── │
│ [Full working example with form state] │
│ │
│ Button group │
│ ─────────── │
│ [Example of button group] │
│ │
│ Confirmation button │
│ ────────────────── │
│ [Example with confirmation dialog] │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ API REFERENCE │
│ ───────────────────────────────────────────────────────────── │
│ │
│ Props │
│ ┌─────────────┬──────────────────┬─────────┬────────────────┐ │
│ │ Prop │ Type │ Default │ Description │ │
│ ├─────────────┼──────────────────┼─────────┼────────────────┤ │
│ │ variant │ 'primary' |... │'primary'│ Visual style │ │
│ │ size │ 'sm'|'md'|'lg' │ 'md' │ Button size │ │
│ │ isLoading │ boolean │ false │ Loading state │ │
│ │ ... │ │ │ │ │
│ └─────────────┴──────────────────┴─────────┴────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ACCESSIBILITY │
│ ───────────────────────────────────────────────────────────── │
│ │
│ • Keyboard: Enter and Space trigger onClick │
│ • Loading state announces to screen readers │
│ • Disabled buttons are focusable for discoverability │
│ │
└─────────────────────────────────────────────────────────────────┘
KEY ELEMENTS:
• Instant copy-paste example at the top
• Live previews for every example
• Common patterns section (real use cases)
• API reference last (not first!)
Copy-Paste-Ready Examples
EXAMPLE QUALITY CHECKLIST:
════════════════════════════════════════════════════════════════════
EVERY EXAMPLE SHOULD BE:
□ COMPLETE
Works if pasted into a file. No missing imports, no "..."
Bad:
<Modal>
...
</Modal>
Good:
import { Modal, Button } from '@myorg/components';
import { useState } from 'react';
function ConfirmDialog() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Delete</Button>
<Modal open={isOpen} onClose={() => setIsOpen(false)}>
<Modal.Header>Confirm delete</Modal.Header>
<Modal.Body>Are you sure you want to delete?</Modal.Body>
<Modal.Footer>
<Button onClick={() => setIsOpen(false)}>Cancel</Button>
<Button variant="destructive">Delete</Button>
</Modal.Footer>
</Modal>
</>
);
}
□ REALISTIC
Shows a real use case, not abstract props.
Bad:
<TextField value={value} onChange={onChange} />
Good:
const [email, setEmail] = useState('');
<TextField
label="Email address"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
/>
□ SELF-CONTAINED
Doesn't require external state management, APIs, etc.
Bad:
<UserAvatar user={currentUser} /> // Where does currentUser come from?
Good:
<Avatar
src="https://example.com/avatar.jpg"
alt="Jane Doe"
fallback="JD"
/>
□ COPYABLE
One-click copy button. Formatted correctly.
Storybook: Use It Right
STORYBOOK DOS AND DON'TS:
════════════════════════════════════════════════════════════════════
DO:
───
• Write stories for every variant and state
• Include realistic example content
• Show composition patterns
• Test edge cases (long text, empty state, error state)
• Make stories copyable/usable as examples
DON'T:
──────
• Rely on Storybook as your only documentation
• Write stories that only show props, not usage
• Forget about mobile/responsive views
• Skip the "common patterns" stories
STORY STRUCTURE:
────────────────
// Button.stories.tsx
export default {
title: 'Components/Button',
component: Button,
};
// Basic usage (first thing people see)
export const Default = () => <Button>Click me</Button>;
// All variants
export const Variants = () => (
<div style={{ display: 'flex', gap: 8 }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
);
// Common real-world patterns
export const FormSubmitWithLoading = () => {
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async () => {
setIsLoading(true);
await new Promise(r => setTimeout(r, 2000));
setIsLoading(false);
};
return (
<Button onClick={handleSubmit} isLoading={isLoading}>
Save changes
</Button>
);
};
// Edge cases
export const WithLongText = () => (
<Button>This is a button with very long text that might overflow</Button>
);
export const InNarrowContainer = () => (
<div style={{ width: 100 }}>
<Button fullWidth>Save</Button>
</div>
);
Theming and Customization
The Customization Spectrum
LEVELS OF CUSTOMIZATION:
════════════════════════════════════════════════════════════════════
Level 0: USE AS-IS
──────────────────
"I just want your defaults."
No work required. Components look like your design system.
Level 1: THEME TOKENS
─────────────────────
"I want to change the primary color."
Change CSS variables or theme tokens.
<ThemeProvider theme={{ colors: { primary: '#007bff' } }}>
<App />
</ThemeProvider>
Level 2: COMPONENT OVERRIDES
────────────────────────────
"I want all my buttons to be rounded."
Override default props or styles for a component type.
const theme = {
components: {
Button: {
defaultProps: {
size: 'large',
},
styleOverrides: {
root: {
borderRadius: '9999px',
},
},
},
},
};
Level 3: INSTANCE STYLING
─────────────────────────
"This specific button needs custom styling."
Style individual instances via className, style, or sx prop.
<Button className="my-custom-button">Special</Button>
<Button sx={{ backgroundColor: 'purple' }}>Purple</Button>
Level 4: COMPONENT WRAPPING
───────────────────────────
"I need to add behavior to your component."
Wrap components for custom functionality.
const MyButton = (props) => {
const handleClick = (e) => {
analytics.track('button_click', { label: props.children });
props.onClick?.(e);
};
return <Button {...props} onClick={handleClick} />;
};
Level 5: HEADLESS/UNSTYLED
──────────────────────────
"I want your behavior, not your styles."
Use unstyled/headless components, bring your own styles.
import { useButton } from '@myorg/components/headless';
function CustomButton(props) {
const { buttonProps } = useButton(props);
return <button {...buttonProps} className="my-styles">...</button>;
}
PRINCIPLE: Support all levels. Don't force Level 5 for Level 1 needs.
Theming Implementation
THEMING APPROACHES:
════════════════════════════════════════════════════════════════════
APPROACH 1: CSS VARIABLES (Recommended)
───────────────────────────────────────
Simplest, most performant, works everywhere.
/* Library provides defaults */
:root {
--color-primary: #0066cc;
--color-primary-hover: #0052a3;
--space-sm: 4px;
--space-md: 8px;
--radius-md: 4px;
--font-sans: system-ui, sans-serif;
}
/* Users override */
:root {
--color-primary: #ff6600;
}
Pros:
✓ No JS required
✓ Works with any framework
✓ Easy to understand
✓ DevTools inspection
✓ Dynamic themes (dark mode) simple
Cons:
✗ Limited to CSS properties
✗ No type safety
APPROACH 2: THEME OBJECT (React Context)
────────────────────────────────────────
More structured, TypeScript-friendly.
const theme = {
colors: {
primary: '#0066cc',
primaryHover: '#0052a3',
},
space: {
sm: '4px',
md: '8px',
},
radii: {
md: '4px',
},
};
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
// Components access via hook or styled-system
const Button = styled.button`
background: ${props => props.theme.colors.primary};
`;
Pros:
✓ Full TypeScript support
✓ Computed values possible
✓ Structured, documented
Cons:
✗ Runtime overhead
✗ Context dependency
✗ Harder to debug
RECOMMENDATION: CSS variables as foundation, optional theme object
for structured overrides. Generate CSS variables from theme object.
Avoiding "Customization Hostage"
DON'T MAKE CUSTOMIZATION ALL-OR-NOTHING:
════════════════════════════════════════════════════════════════════
BAD: "If you want to change one thing, you have to redefine everything"
.button {
/* All styles in one block */
padding: 8px 16px;
background: blue;
color: white;
border-radius: 4px;
font-weight: 600;
/* ... 20 more properties */
}
User: "I just want to change the border-radius..."
→ Has to override everything or fight specificity
GOOD: Granular customization via tokens
.button {
padding: var(--button-padding-y) var(--button-padding-x);
background: var(--button-bg);
color: var(--button-text);
border-radius: var(--button-radius);
font-weight: var(--button-font-weight);
}
User: "I just want to change the border-radius..."
→ Sets --button-radius: 9999px; Done.
ALSO GOOD: Targeted style overrides
const theme = {
components: {
Button: {
styleOverrides: {
root: { borderRadius: '9999px' },
// Don't have to touch other styles
},
},
},
};
Versioning and Breaking Changes
Versioning Strategy
SEMANTIC VERSIONING FOR COMPONENT LIBRARIES:
════════════════════════════════════════════════════════════════════
MAJOR (X.0.0): Breaking changes
───────────────────────────────
• Removing components
• Removing props
• Changing prop types incompatibly
• Changing default behavior significantly
• Dropping browser/React version support
MINOR (0.X.0): New features, backward-compatible
────────────────────────────────────────────────
• New components
• New props on existing components
• New variants
• New hooks
• Bug fixes that might change appearance slightly
PATCH (0.0.X): Bug fixes
────────────────────────
• Bug fixes
• Documentation updates
• Performance improvements (no API changes)
• Accessibility fixes
THE PROBLEM WITH VISUAL CHANGES:
════════════════════════════════════════════════════════════════════
Semantic versioning doesn't capture "visual breaking changes."
<Button /> looks different after update
→ Not a "breaking change" (API is same)
→ But breaks visual regression tests
→ And confuses users
SOLUTION: Treat significant visual changes as breaking.
Document visual changes prominently in changelogs.
Consider visual-breaking vs api-breaking distinction in versioning.
Minimizing Breaking Changes
STRATEGIES TO AVOID BREAKING CHANGES:
════════════════════════════════════════════════════════════════════
1. ADD, DON'T REMOVE
────────────────────
Adding props is non-breaking. Removing is breaking.
Bad:
// v1
<Button type="primary" />
// v2 - removed type, added variant
<Button variant="primary" /> // BREAKING
Good:
// v2 - add variant, deprecate type, keep type working
<Button variant="primary" /> // new way
<Button type="primary" /> // still works, shows deprecation warning
2. DEPRECATE BEFORE REMOVING
────────────────────────────
Give users time to migrate.
// Version 2.0
const Button = ({ type, variant, ...props }) => {
if (type && !variant) {
console.warn(
'Button: `type` prop is deprecated. Use `variant` instead. ' +
'`type` will be removed in version 3.0.'
);
variant = type; // Keep it working
}
// ...
};
// Version 3.0
// Now safe to remove `type`
3. USE PROP SPREADING
─────────────────────
Let users pass arbitrary props. Reduces pressure to add props.
// Users can add any HTML attribute without library changes
<Button data-testid="submit" aria-describedby="help">
Save
</Button>
4. PROVIDE CODEMODS
───────────────────
For unavoidable breaking changes, provide automated migration.
npx @myorg/components-codemod v2-to-v3
Automatically transforms:
<Button type="primary" /> → <Button variant="primary" />
5. BATCH BREAKING CHANGES
─────────────────────────
If you must break, break everything at once.
Bad: Breaking changes every few months
Good: Save breaking changes for major versions, release annually
Changelog Communication
CHANGELOG THAT HELPS:
════════════════════════════════════════════════════════════════════
## v2.0.0 (2024-02-01)
### Breaking Changes
#### Button: `type` prop renamed to `variant`
The `type` prop has been renamed to `variant` for consistency.
**Migration:**
```diff
- <Button type="primary">Click</Button>
+ <Button variant="primary">Click</Button>
Or run: npx @myorg/components-codemod button-type-to-variant
Modal: onRequestClose renamed to onClose
Migration:
- <Modal onRequestClose={handleClose}>
+ <Modal onClose={handleClose}>
New Features
New: Toast component
import { toast } from '@myorg/components';
toast.success('Changes saved!');
toast.error('Something went wrong');
See Toast documentation.
Bug Fixes
- Fixed Button focus ring not visible in Safari
- Fixed Modal not trapping focus correctly
- Fixed TextField label not associated with input
PRINCIPLES: • Show before/after code • Provide codemods when possible • Group by impact (breaking first) • Link to full documentation
---
## Governance and Contribution
### Contribution Model
CONTRIBUTION MODELS: ════════════════════════════════════════════════════════════════════
MODEL 1: CENTRALIZED TEAM ───────────────────────── A dedicated team owns and develops the library. Others submit requests; the team implements.
Pros: ✓ Consistent quality ✓ Coherent vision ✓ Dedicated support
Cons: ✗ Bottleneck ✗ May not understand all use cases ✗ Team becomes "the design system police"
Best for: Organizations with resources for dedicated team
MODEL 2: OPEN CONTRIBUTION ────────────────────────── Anyone can contribute. Library team reviews.
Pros: ✓ Scales better ✓ Contributors understand their use cases ✓ Broader ownership
Cons: ✗ Quality variance ✗ Review burden ✗ Harder to maintain vision
Best for: Organizations with strong engineering culture
MODEL 3: FEDERATED (Recommended for most) ───────────────────────────────────────── Core team owns foundational components. Product teams can contribute domain components. Clear ownership model.
Core (library team owns): • Button, Input, Modal, etc. • Theming system • Guidelines and patterns
Domain (product teams own): • InvoiceTable (billing team) • UserAvatar (user team) • ChartWidget (analytics team)
Core team reviews for consistency, domain teams maintain.
### Contribution Guidelines
CONTRIBUTION PROCESS: ════════════════════════════════════════════════════════════════════
- PROPOSAL (Before writing code) ───────────────────────────────── Open an issue or RFC describing: • What component/feature you want to add • Why it's needed (use cases) • Proposed API • Whether design has approved (for visual components)
Library team responds within 1 week with: • Approval to proceed • Requests for changes • Rejection with explanation
- IMPLEMENTATION ───────────────── Follow contribution checklist:
□ Component implementation □ TypeScript types □ Unit tests □ Storybook stories □ Documentation page □ Accessibility testing □ Changelog entry
- REVIEW ───────── Library team reviews for: • API consistency with existing components • Code quality • Accessibility • Documentation completeness • Test coverage
Turnaround: Within 1 week for initial review
- RELEASE ────────── Library team merges and releases. Contributor credited in changelog.
COMPONENT CHECKLIST: ════════════════════════════════════════════════════════════════════
Before a component is accepted:
FUNCTIONALITY □ Works as documented □ Handles edge cases (empty, loading, error) □ TypeScript types are accurate and complete
ACCESSIBILITY □ Keyboard navigable □ Screen reader tested □ ARIA attributes correct □ Focus management appropriate
TESTING □ Unit tests for behavior □ Visual regression test □ Accessibility tests (axe)
DOCUMENTATION □ Basic usage example □ All props documented □ Common patterns shown □ Storybook stories
CONSISTENCY □ Follows existing patterns □ Uses design tokens □ API matches similar components
### Handling Requests
REQUEST TRIAGE: ════════════════════════════════════════════════════════════════════
When teams request new components or features:
ACCEPT if: ────────── • Multiple teams need it • Design team has approved visuals • Fits the library's scope • Team is willing to contribute or wait
DEFER if: ───────── • Only one team needs it currently • Design hasn't finalized • Resources aren't available • Can be built in userland temporarily
REJECT if: ────────── • Too specific to one product • Conflicts with design system principles • Would complicate existing components significantly • Better solved another way
COMMUNICATION TEMPLATE: ────────────────────────
Thank you for the request!
[ACCEPTED] "This looks like a great addition. Here's how to proceed:
- [Next steps]
- [Timeline expectation]"
[DEFERRED] "We like this idea but can't prioritize it now because [reason]. In the meantime, here's how you can build this in your project: [workaround]. We'll revisit in [timeframe]."
[REJECTED] "After discussion, we've decided not to add this because [reason]. Here's what we recommend instead: [alternative]."
---
## Measuring Success
### Adoption Metrics
METRICS THAT MATTER: ════════════════════════════════════════════════════════════════════
ADOPTION ──────── • Teams using the library (count and %) • Components used per team • Import frequency in codebase • Growth over time
How to measure: • Bundle analysis • Codebase grep/search • Survey
Target: 80%+ of frontend code uses library components
COVERAGE ──────── • % of UI built with library components vs custom • Components that teams build themselves (gaps)
How to measure: • Code review sampling • Survey • AST analysis of codebases
Target: <20% custom components for common patterns
SATISFACTION ──────────── • Developer satisfaction score (survey) • NPS for the library • Qualitative feedback
How to measure: • Quarterly survey • Feedback channels
Target: Positive NPS, >4/5 satisfaction
QUALITY ─────── • Bugs reported per component • Accessibility audit results • Performance metrics (bundle size, runtime)
Target: <1 bug per component per quarter
VELOCITY ──────── • Time from request to release • Issue response time • PR review turnaround
Target: <1 week average issue response
ANTI-METRICS (Don't optimize for these alone): ────────────────────────────────────────────── • Component count (more isn't better) • Lines of code (more isn't better) • Downloads (vanity metric)
### The Adoption Dashboard
┌─────────────────────────────────────────────────────────────────────┐ │ COMPONENT LIBRARY HEALTH DASHBOARD │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ADOPTION Q1 2024 │ │ ───────────────────────────────────────────────────────────────── │ │ Teams using library 12/15 80% ████████░░ │ │ Components in use 34/42 81% ████████░░ │ │ Most used: Button, Input, Modal, Card, Select │ │ Least used: Breadcrumb, Skeleton, Timeline │ │ │ │ SATISFACTION │ │ ───────────────────────────────────────────────────────────────── │ │ Developer NPS +42 ▲ +8 vs Q4 │ │ Satisfaction score 4.2/5 ▲ +0.3 vs Q4 │ │ "Easy to use" 4.4/5 │ │ "Well documented" 3.8/5 ← Needs work │ │ "Responsive support" 4.5/5 │ │ │ │ QUALITY │ │ ───────────────────────────────────────────────────────────────── │ │ Bugs reported 8 ▼ from 14 │ │ Avg time to fix 4 days │ │ a11y audit score 94% ▲ from 89% │ │ Bundle size (core) 48KB │ │ │ │ VELOCITY │ │ ───────────────────────────────────────────────────────────────── │ │ Feature requests open 12 │ │ Avg request → release 21 days ← Needs work │ │ Avg issue response 1.2 days │ │ │ │ TOP ISSUES │ │ ───────────────────────────────────────────────────────────────── │ │ 1. Need DataTable component (5 requests) │ │ 2. Modal animation not smooth (3 reports) │ │ 3. Better dark mode support (4 requests) │ │ │ └─────────────────────────────────────────────────────────────────────┘
### Developer Survey
QUARTERLY DEVELOPER SURVEY: ════════════════════════════════════════════════════════════════════
Rate your agreement (1-5):
USABILITY □ The components are easy to use □ I can usually find what I need □ The documentation helps me solve problems □ I can customize components when needed
QUALITY □ The components work reliably □ I rarely encounter bugs □ The components are accessible □ Performance is acceptable
SUPPORT □ I get responses when I have questions □ Feature requests are addressed □ The team communicates changes well □ I feel heard when I give feedback
OPEN QUESTIONS □ What components are missing that you need? □ What's the most frustrating thing about the library? □ What would make you more likely to use the library? □ What do you like most about the library?
NPS QUESTION □ How likely are you to recommend this library to a colleague? (0-10)
---
## Common Patterns and Anti-Patterns
### Anti-Patterns
┌─────────────────────────────────────────────────────────────────────┐ │ COMPONENT LIBRARY ANTI-PATTERNS │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ THE KITCHEN SINK │ │ ───────────────────────────────────────────────────────────────── │ │ Problem: Component with 50+ props trying to handle every case. │ │ Result: Impossible to learn, hard to maintain, poor tree-shaking.│ │ Fix: Composition over configuration. Break into smaller pieces. │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ THE NOT-INVENTED-HERE │ │ ───────────────────────────────────────────────────────────────── │ │ Problem: Building everything from scratch instead of using │ │ proven headless libraries (Radix, Headless UI, React Aria). │ │ Result: Bugs in complex components, reinventing accessibility. │ │ Fix: Use headless libraries for complex components, add styling. │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ THE STYLE PRISON │ │ ───────────────────────────────────────────────────────────────── │ │ Problem: Styles are impossible to override without !important. │ │ Result: Teams fork components or don't use them. │ │ Fix: Use CSS variables, lower specificity, className passthrough.│ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ THE BREAKING CHANGE FACTORY │ │ ───────────────────────────────────────────────────────────────── │ │ Problem: Frequent breaking changes that require migration. │ │ Result: Teams lock versions and stop updating. │ │ Fix: Deprecate before removing, provide codemods, batch breaks. │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ THE DOCUMENTATION DESERT │ │ ───────────────────────────────────────────────────────────────── │ │ Problem: Props table exists, but no real examples or patterns. │ │ Result: Developers don't know how to use components correctly. │ │ Fix: Copy-paste examples, common patterns, realistic content. │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ THE SUPPORT VACUUM │ │ ───────────────────────────────────────────────────────────────── │ │ Problem: Issues sit for months, questions go unanswered. │ │ Result: Teams lose trust and build their own solutions. │ │ Fix: Budget for support. Respond within days, not weeks. │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ THE APPROVAL LABYRINTH │ │ ───────────────────────────────────────────────────────────────── │ │ Problem: Contributing requires design review, accessibility │ │ review, architecture review, code review, legal review... │ │ Result: No one contributes. Library team is bottleneck. │ │ Fix: Streamline process. Trust but verify. Batch approvals. │ │ │ └─────────────────────────────────────────────────────────────────────┘
### Patterns That Work
┌─────────────────────────────────────────────────────────────────────┐ │ PATTERNS FOR SUCCESSFUL LIBRARIES │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ START SMALL AND GROW │ │ ───────────────────────────────────────────────────────────────── │ │ Launch with 10 solid components, not 50 mediocre ones. │ │ Add based on actual demand. │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ DOG-FOOD HEAVILY │ │ ───────────────────────────────────────────────────────────────── │ │ Library team should use their own components daily. │ │ You'll find UX issues that users won't report. │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ OFFICE HOURS │ │ ───────────────────────────────────────────────────────────────── │ │ Weekly session where teams can ask questions, request features, │ │ and give feedback directly. │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ MIGRATION GUIDES FOR EVERYTHING │ │ ───────────────────────────────────────────────────────────────── │ │ From nothing → library. From old version → new version. │ │ From competitor library → yours. Make migration easy. │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ VISIBLE LEADERSHIP SUPPORT │ │ ───────────────────────────────────────────────────────────────── │ │ Engineering leadership publicly endorses and uses the library. │ │ "We use the component library" is the default, not the exception.│ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ ESCAPE HATCHES EVERYWHERE │ │ ───────────────────────────────────────────────────────────────── │ │ className, style, ref, render props, unstyled variants. │ │ Never trap users. Let them escape when needed. │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ CELEBRATE ADOPTERS │ │ ───────────────────────────────────────────────────────────────── │ │ Highlight teams that adopt. Share success stories. │ │ Make adoption visible and valued. │ │ │ └─────────────────────────────────────────────────────────────────────┘
---
## Quick Reference
┌─────────────────────────────────────────────────────────────────────┐ │ COMPONENT LIBRARY QUICK REFERENCE │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ADOPTION PRINCIPLES │ │ ───────────────────────────────────────────────────────────────── │ │ 1. Make simple things simple │ │ 2. Make complex things possible │ │ 3. Be predictable │ │ 4. Composition over configuration │ │ 5. Play nice with ecosystem │ │ 6. Document for adoption, not completeness │ │ 7. Support is part of the product │ │ │ │ API DESIGN CHECKLIST │ │ ───────────────────────────────────────────────────────────────── │ │ □ Sensible defaults (works without props) │ │ □ Familiar patterns (match React/DOM conventions) │ │ □ <10 common props per component │ │ □ TypeScript types with literal unions │ │ □ Spread remaining props to root element │ │ □ Compound components for complex cases │ │ │ │ DOCUMENTATION CHECKLIST │ │ ───────────────────────────────────────────────────────────────── │ │ □ Copy-paste example at top of every page │ │ □ Live previews for all examples │ │ □ Common patterns section │ │ □ Complete, realistic example code │ │ □ API reference (props table) │ │ □ Accessibility notes │ │ │ │ COMPONENT CHECKLIST │ │ ───────────────────────────────────────────────────────────────── │ │ □ Works without configuration │ │ □ Keyboard accessible │ │ □ Screen reader tested │ │ □ Unit tests │ │ □ Storybook stories │ │ □ Documentation page │ │ □ TypeScript types │ │ │ │ VERSIONING RULES │ │ ───────────────────────────────────────────────────────────────── │ │ • Add props = minor version │ │ • Remove/rename props = major version │ │ • Deprecate before removing │ │ • Provide codemods for breaking changes │ │ • Batch breaking changes │ │ │ │ ADOPTION METRICS │ │ ───────────────────────────────────────────────────────────────── │ │ • Teams using library: target 80%+ │ │ • Developer satisfaction: target 4+/5 │ │ • Issue response time: target <1 week │ │ • Custom component rate: target <20% │ │ │ │ ANTI-PATTERNS TO AVOID │ │ ───────────────────────────────────────────────────────────────── │ │ ✗ 50-prop mega-components │ │ ✗ Documentation without examples │ │ ✗ Frequent breaking changes │ │ ✗ Unanswered issues and questions │ │ ✗ Styles that can't be overridden │ │ ✗ Missing escape hatches │ │ │ └─────────────────────────────────────────────────────────────────────┘
---
## Conclusion
A component library succeeds not when it's technically perfect, but when teams choose to use it.
That choice happens every time a developer faces a UI task. They can use your library, use something else, or build it themselves. You're competing for that choice.
To win that competition consistently:
**Make the easy path your library.** If your Button takes longer to use than writing a `<button>`, you've lost. Sensible defaults, copy-paste examples, and predictable behavior make your library the path of least resistance.
**Earn trust through stability.** Every breaking change is a withdrawal from your trust account. Every bug is a withdrawal. Every unanswered question is a withdrawal. Protect the trust you've built.
**Treat support as product work.** Unanswered issues signal abandonware. Quick, helpful responses signal a supported product. Budget for support like you budget for features.
**Measure adoption, not just quality.** A perfectly accessible, well-tested component that no one uses is a failure. Track adoption metrics and treat declining adoption as a bug.
**Stay close to your users.** Office hours, surveys, Slack channels—whatever it takes to understand what teams actually need and what's blocking adoption.
The goal isn't a component library. The goal is consistent, accessible, maintainable UI across your organization. The component library is just the mechanism. Keep your eyes on adoption, and the rest will follow.
What did you think?