
Components Build
- 167 installs
- 53 repo stars
- Updated January 28, 2026
- nolly-studio/components-build-skill
Helps with ai & agent building tasks.
About
components-build is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- components-build
- AI & Agent Building
- AI-coding skill
Components Build by the numbers
- 167 all-time installs (skills.sh)
- Ranked #3,181 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nolly-studio/components-build-skill --skill components-buildAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 167 |
|---|---|
| repo stars | ★ 53 |
| Last updated | January 28, 2026 |
| Repository | nolly-studio/components-build-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
Components.build Specification
Comprehensive guidelines for building modern, composable, and accessible UI components. Contains 16 rule categories covering everything from core principles to distribution, co-authored by Hayden Bleasel and shadcn.
When to Apply
Reference these guidelines when:
- Creating new React components or component libraries
- Designing component APIs and prop interfaces
- Implementing accessibility features (keyboard, ARIA, focus management)
- Building composable component architectures
- Styling components with Tailwind CSS and CVA
- Publishing components to registries or npm
Rule Categories by Priority
| Priority | Category | Focus | Prefix |
|---|---|---|---|
| 1 | Overview | Specification scope and goals | overview |
| 2 | Principles | Core design philosophy | principles |
| 3 | Definitions | Common terminology | definitions |
| 4 | Composition | Breaking down complex components | composition |
| 5 | Accessibility | Keyboard, screen readers, ARIA | accessibility |
| 6 | State | Controlled/uncontrolled patterns | state |
| 7 | Types | TypeScript props and interfaces | types |
| 8 | Polymorphism | Element switching with as prop | polymorphism |
| 9 | As-Child | Radix Slot composition pattern | as-child |
| 10 | Data Attributes | data-state and data-slot | data-attributes |
| 11 | Styling | Tailwind CSS, cn utility, CVA | styling |
| 12 | Design Tokens | CSS variables and theming | design-tokens |
| 13 | Documentation | Component documentation | documentation |
| 14 | Registry | Component registries | registry |
| 15 | NPM | Publishing to npm | npm |
| 16 | Marketplaces | Component marketplaces | marketplaces |
Quick Reference
1. Overview
overview- Specification scope, goals, and philosophy
2. Principles
principles- Composability, accessibility, customization, transparency
3. Definitions
definitions- Common terminology (primitive, compound, headless, etc.)
4. Composition
composition-root- Root component with Context for shared statecomposition-item- Item wrapper componentscomposition-trigger- Interactive trigger componentscomposition-content- Content display componentscomposition-export- Namespace export pattern
5. Accessibility
accessibility-semantic-html- Use appropriate HTML elementsaccessibility-keyboard- Full keyboard navigation supportaccessibility-aria- Proper ARIA roles, states, and propertiesaccessibility-focus- Focus management and restorationaccessibility-live-regions- Screen reader announcementsaccessibility-contrast- Color contrast requirements
6. State
state-uncontrolled- Internal state managementstate-controlled- External state delegationstate-controllable- Support both patterns with useControllableState
7. Types
types-extend-html- Extend native HTML attributestypes-export- Export prop types for consumerstypes-single-element- One component wraps one element
8. Polymorphism
polymorphism-as-prop- Change rendered element typepolymorphism-typescript- Type-safe polymorphic componentspolymorphism-defaults- Semantic element defaults
9. As-Child
as-child-slot- Radix Slot for prop mergingas-child-composition- Compose with child components
10. Data Attributes
data-attributes-state- Usedata-statefor styling statesdata-attributes-slot- Usedata-slotfor targeting sub-components
11. Styling
styling-cn-utility- Combine clsx and tailwind-mergestyling-order- Base → Variants → Conditionals → User overridesstyling-cva- Class Variance Authority for variantsstyling-css-variables- Dynamic values with CSS variables
12. Design Tokens
design-tokens-css-variables- Define tokens as CSS variablesdesign-tokens-theming- Support light/dark modes and themes
13. Documentation
documentation-props- Document all props with JSDocdocumentation-examples- Provide usage examples
14. Registry
registry-structure- Registry file structureregistry-schema- Component metadata schema
15. NPM
npm-package-json- Package configurationnpm-exports- Module exports
16. Marketplaces
marketplaces-distribution- Component distribution strategies
How to Use
Read individual rule files for detailed explanations and code examples:
rules/composition/SKILL.md
rules/accessibility/SKILL.md
rules/styling/SKILL.mdEach rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Best practices and common pitfalls
Full Compiled Document
For the complete guide with all rules expanded: AGENTS.md
Key Principles
1. Composition over Configuration - Break components into composable sub-components 2. Accessibility by Default - Not an afterthought, but a requirement 3. Single Element Wrapping - Each component wraps one HTML element 4. Extend HTML Attributes - Always extend native element props 5. Export Types - Make prop types available to consumers 6. Support Both State Patterns - Controlled and uncontrolled 7. Intelligent Class Merging - Use cn() utility with tailwind-merge
Authors
Co-authored by:
- Hayden Bleasel (@haydenbleasel)
- shadcn (@shadcn)
Adapted as an AI skill by:
- Jordan Gilliam (@nolansym)
Based on the components.build specification.
Components.build Specification
Version 1.0.0 components.build January 2026
Note:
This document is mainly for agents and LLMs to follow when maintaining,
generating, or refactoring UI component libraries. Humans
may also find it useful, but guidance here is optimized for automation
and consistency by AI-assisted workflows.
---
Abstract
Comprehensive guidelines for building modern, composable, and accessible UI components. This specification provides patterns and best practices for creating React components that are composable, accessible, customizable, and well-documented.
---
Table of Contents
1. Overview — MEDIUM
2. Principles — HIGH
3. Definitions — MEDIUM
4. Composition — HIGH
5. Accessibility — CRITICAL
6. State — HIGH
7. Types — HIGH
- 7.1 Component Types
8. Polymorphism — MEDIUM
9. As-Child — MEDIUM
- 9.1 asChild Pattern
10. Data Attributes — LOW
11. Styling — HIGH
12. Design Tokens — MEDIUM
- 12.1 Design Tokens
13. Documentation — MEDIUM
14. Registry — LOW
- 14.1 Component Registries
15. NPM — LOW
- 15.1 Publishing to NPM
16. Marketplaces — LOW
---
1. Overview
Impact: MEDIUM
Specification scope, goals, and philosophy. Introduction to the components.build standard for building modern UI components.
1.1 Components.build Overview
Impact: MEDIUM (Foundation for understanding the specification)
The components.build specification is an open-source standard for building modern, composable, and accessible UI components. It provides high-level guidelines, best practices, and common terminology for designing UI components that integrate smoothly into any codebase.
What This Specification Is:
This spec is not:
- A tutorial or course on React
- A promotion for any specific component library or registry
- A replacement for framework documentation
This spec is:
- A set of high-level guidelines and best practices
- A common terminology for designing UI components
- A standard for ensuring components meet modern expectations
- A framework for creating components that integrate smoothly across projects
Who This Is For:
This specification is written for:
- Open-source maintainers building and distributing component libraries
- Senior front-end engineers designing component APIs and design systems
- Developers familiar with JavaScript/TypeScript and React
Framework Scope:
While examples use React (with JSX/TSX) for concreteness, the fundamental concepts apply to other frameworks (Vue, Svelte, Angular). The philosophy is framework-agnostic.
Core Goals:
The specification aims to help developers create components that are:
1. Composable - Components combine and nest to create complex UIs
2. Accessible - Usable by everyone, including users with disabilities
3. Easy to adopt - Integrate smoothly into any codebase
4. Consistent - Follow modern expectations and patterns
5. Well-documented - Clear guidelines and terminology
Key Philosophy:
- Composition over configuration - Build flexible, composable APIs
- Accessibility by default - Not an afterthought, but a requirement
- Developer experience - Components should be easy to understand, customize, and integrate
- Transparency - Source code should be inspectable and modifiable
- Standards alignment - Follow web standards and modern best practices
Example:
The following examples illustrate the difference between components that don't follow the specification and those that do:
Incorrect:
// Hard-coded styles, no accessibility, not composable
function Button() {
return (
<button style={{ backgroundColor: '#007bff', color: 'white', padding: '10px' }}>
Click me
</button>
);
}Correct:
// Composable, accessible, customizable
import { cn } from '@/lib/utils';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'outline';
}
export function Button({
className,
variant = 'default',
children,
...props
}: ButtonProps) {
return (
<button
className={cn(
'inline-flex items-center justify-center rounded-md px-4 py-2',
'focus-visible:outline-none focus-visible:ring-2',
variant === 'default' && 'bg-primary text-primary-foreground',
variant === 'outline' && 'border border-input bg-background',
className
)}
{...props}
>
{children}
</button>
);
}Specification Authors:
Co-authored by:
- Hayden Bleasel (@haydenbleasel)
- shadcn (@shadcn)
Adapted as an AI skill by:
- Jordan Gilliam (@nolansym)
Reference: https://components.build
---
2. Principles
Impact: HIGH
Core design principles for component architecture including composability, accessibility, customization, performance, transparency, and DX.
2.1 Core Component Principles
Impact: HIGH (Foundation of component architecture decisions)
These six principles guide all component design decisions. Apply them consistently when building, refactoring, or evaluating components.
Favor composition over inheritance. Build components that combine and nest to create complex UIs.
Incorrect: monolithic, hard to customize
<Accordion data={data} />Correct: composable, each layer customizable
<Accordion.Root open={open} onOpenChange={setOpen}>
{items.map((item) => (
<Accordion.Item key={item.id}>
<Accordion.Trigger>{item.title}</Accordion.Trigger>
<Accordion.Content>{item.content}</Accordion.Content>
</Accordion.Item>
))}
</Accordion.Root>Components must be usable by all users. Accessibility is not optional—it's a baseline feature.
Incorrect: generic div with click handler
<div onClick={handleClick} className="button">Click me</div>Correct: semantic button element
<button onClick={handleClick}>Click me</button>Best practices:
- Use semantic HTML (
<button>,<ul>/<li>,<nav>, etc.)
- Provide proper ARIA roles, states, and properties
- Support keyboard navigation for all interactive elements
- Ensure focus management in modals, dropdowns, and overlays
Components should be easy to restyle or adapt to different design requirements.
Correct: design tokens and className override
export function Button({ className, ...props }: ButtonProps) {
return (
<button
className={cn('base-button-styles', className)}
{...props}
/>
);
}Best practices:
- Use CSS variables (design tokens) for colors, spacing, typography
- Always allow
classNameprop for style overrides
- Provide sensible default styling
- Avoid hard-coded colors, fonts, or spacing values
Components should be lean in terms of assets and dependencies.
Incorrect: heavy dependency for simple task
import { entireDateLibrary } from 'heavy-date-lib';Correct: native APIs or lightweight alternatives
const formatDate = (date: Date) => date.toLocaleDateString();Best practices:
- Keep bundle size minimal
- Use tree-shaking friendly imports
- Lazy load heavy features when possible
- Optimize for initial render performance
Components should not be black boxes. Developers should be able to inspect and modify them.
Correct: clear, readable implementation
export function Button({ children, onClick, ...props }: ButtonProps) {
return (
<button onClick={onClick} {...props}>
{children}
</button>
);
}Incorrect: obfuscated or overly complex
export const Button = compose(withHOC1, withHOC2, withHOC3)(BaseButton);Components should come with clear documentation and examples.
Correct: comprehensive JSDoc
/**
* Button component for primary actions.
*
* @example
* <Button variant="primary" onClick={handleClick}>Click me</Button>
*
* @remarks
* - Supports keyboard navigation (Enter/Space)
* - Accessible by default with proper ARIA attributes
*/
export function Button({ ... }: ButtonProps) { }A well-designed component applies all six principles together: Composes with other components, works for everyone with proper accessibility, adapts to different designs via theming, performs efficiently, can be inspected and modified, and is easy to learn through documentation.
---
3. Definitions
Impact: MEDIUM
Common terminology and component type definitions including primitive, compound, headless, and styled components.
3.1 Component Artifact Definitions
Impact: MEDIUM (Essential for correct component classification)
Precise terminology for classifying and naming UI artifacts.
The lowest-level building block providing behavior and accessibility without styling. Completely headless.
Examples: Radix UI Primitives, React Aria Components, Headless UI
A styled, reusable UI unit that adds visual design to primitives or composes multiple elements.
Examples: shadcn/ui components, Material UI, Ant Design
A specific composition solving a UI/UX problem. Documentation-focused, not a reusable component.
Examples: Form validation with inline errors, confirming destructive actions, typeahead search
An opinionated, production-ready composition solving a concrete interface use case.
Examples: Pricing table, auth screens, onboarding stepper, AI chat panel
A complete, single-route view composed of multiple blocks.
A multi-page collection or full-site scaffold bundling pages, routing, layouts, and providers.
Examples: SaaS starter, e-commerce template, dashboard starter
A helper for developer ergonomics or composition; not rendered UI.
Examples: React hooks, class utilities, keybinding helpers, focus scopes
1. Behavior/a11y only, no styling? → Primitive
2. Styled, reusable UI element? → Component
3. Concrete product use case with opinionated composition? → Block
4. Multi-page scaffold with routing/providers? → Template
5. Documentation of recurring solution? → Pattern
6. Non-visual logic? → Utility
Incorrect: mislabeling a styled component as a primitive
// Wrong: This is styled, so it's a Component, not a Primitive
function DialogPrimitive({ open, onOpenChange, children }) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Content className="rounded-lg border bg-white p-6 shadow-lg">
{children}
</Dialog.Content>
</Dialog.Root>
);
}Correct: proper classification based on styling presence
// Primitive - behavior only, no styling
function DialogPrimitive({ open, onOpenChange, children }) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
{children}
</Dialog.Root>
);
}
// Component - styled wrapper around primitive
function Dialog({ open, onOpenChange, children }) {
return (
<DialogPrimitive open={open} onOpenChange={onOpenChange}>
<Dialog.Content className={cn("rounded-lg border bg-white p-6 shadow-lg")}>
{children}
</Dialog.Content>
</DialogPrimitive>
);
}- Props API - Public configuration surface, typed and documented
- Children/Slots - Placeholders for caller-provided structure
- Render Prop - Function child delegating rendering
- Controlled - Value driven by props (parent is source of truth)
- Uncontrolled - Internal state with optional
defaultValue
- Provider/Context - Supplies shared state to subtree
- Portal - Rendering outside DOM hierarchy for layering
- Headless - Behavior without styling
- Styled - Ships with default visual design
- Variants - Style/behavior permutations via props
- Design Tokens - Named values for theming
---
4. Composition
Impact: HIGH
Breaking down complex components into composable sub-components using Root, Trigger, Content, and other naming patterns.
4.1 Component Composition
Impact: HIGH (Foundation of component architecture)
Composition is the foundation of building modern UI components. Instead of cramming all functionality into a single component with dozens of props, break components down into smaller, focused sub-components that work together.
Incorrect: monolithic, hard to customize
<Accordion data={data} />Correct: composable, each layer customizable
export const Accordion = { Root, Item, Trigger, Content };
// Usage
<Accordion.Root open={open} setOpen={setOpen}>
<Accordion.Item>
<Accordion.Trigger>Title</Accordion.Trigger>
<Accordion.Content>Content</Accordion.Content>
</Accordion.Item>
</Accordion.Root>The main container that holds sub-components and manages shared state using Context:
A simple wrapper for each item:
Handles user interaction:
Displays the main content:
Export components as a namespace:
- Root - Main container component
- Trigger - Element that initiates an action
- Content - Element containing the main content
- Header/Body/Footer - Structured content areas
- Title/Description - Informational components
1. Single Responsibility - Each sub-component should have one clear purpose
2. Context for State - Use React Context to share state between sub-components
3. Extend HTML Attributes - Always extend native HTML element props
4. Consistent Naming - Follow established conventions
5. Namespace Exports - Export components as a namespace for clean API
6. Composition Over Configuration - Prefer multiple components over many props
Use composition when:
- A component has multiple responsibilities
- Customization requires CSS overrides
- You need flexible layouts or structures
- Building a component library
- Components need to work together but remain independent
Avoid composition for:
- Simple, single-purpose components
- Components that don't need customization
- Over-engineering simple UI elements
---
5. Accessibility
Impact: CRITICAL
Building accessible components with keyboard navigation, screen reader support, ARIA attributes, and focus management.
5.1 Accessibility Guidelines
Impact: CRITICAL (Essential for inclusive component design)
Accessibility (a11y) is not optional—it's a fundamental requirement. Every component must be usable by everyone, including people with visual, motor, auditory, or cognitive disabilities.
Always start with the most appropriate HTML element:
Incorrect: generic div
<div onClick={handleClick} className="button">Click me</div>Correct: semantic element
// Polite announcement
<div role="status" aria-live="polite">{savedMessage && "Saved"}</div>
// Assertive announcement
<div role="alert" aria-live="assertive">{errorMessage}</div>Every interactive element must be keyboard accessible:
Use ARIA attributes when necessary:
Support users with visual impairments:
1. Don't use ARIA if you can use semantic HTML
2. Don't change native semantics unless necessary
3. All interactive elements must be keyboard accessible
4. Don't hide focusable elements - Never use aria-hidden="true" on focusable elements
5. All interactive elements must have accessible names
Placeholder as labels:
// ❌ Placeholder disappears
<input placeholder="Email address" />
// ✅ Persistent label
<label>Email address <input type="email" /></label>Empty buttons:
// ❌ No accessible name
<button><TrashIcon /></button>
// ✅ Screen reader text
<button aria-label="Delete item"><TrashIcon aria-hidden="true" /></button>Disabled elements:
<!-- Allow zooming -->
<meta name="viewport" content="width=device-width, initial-scale=1">---
6. State
Impact: HIGH
Controlled and uncontrolled state patterns using useControllableState for flexible component APIs.
6.1 State Management Patterns
Impact: HIGH (Flexible component APIs for all use cases)
Build flexible components that work seamlessly in both controlled and uncontrolled modes.
Component manages its own state internally:
When to use: Simple components, default behavior, internal state is sufficient.
Parent component manages the state:
When to use: State coordination, external data sources, form validation, persistence.
Use @radix-ui/react-use-controllable-state to support both:
Usage:
import { useControllableState } from '@radix-ui/react-use-controllable-state';
type ToggleProps = {
checked?: boolean;
defaultChecked?: boolean;
onCheckedChange?: (checked: boolean) => void;
disabled?: boolean;
};
export const Toggle = ({ checked: controlledChecked, defaultChecked, onCheckedChange, disabled }: ToggleProps) => {
const [checked, setChecked] = useControllableState({
prop: controlledChecked,
defaultProp: defaultChecked ?? false,
onChange: onCheckedChange,
});
return (
<button
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => !disabled && setChecked(!checked)}
>
{checked ? 'On' : 'Off'}
</button>
);
};| State Type | Value Prop | Default Prop | Change Callback |
|------------|------------|--------------|-----------------|
| Generic | value | defaultValue | onValueChange |
| Boolean | checked | defaultChecked | onCheckedChange |
| Open/Close | open | defaultOpen | onOpenChange |
| Selection | selected | defaultSelected | onSelectedChange |
1. Always Make Props Optional:
// ✅ Supports both patterns
type Props = {
value?: number;
defaultValue?: number;
onValueChange?: (value: number) => void;
};2. Provide Sensible Defaults:
const [value, setValue] = useControllableState({
prop: controlledValue,
defaultProp: defaultValue ?? 0,
onChange: onValueChange,
});3. Handle onChange in Both Modes:
The onChange callback should fire regardless of controlled/uncontrolled mode.
1. Flexibility - Works in both controlled and uncontrolled modes
2. Developer Experience - Simple API for simple cases, powerful for complex
3. Consistency - Matches patterns used by Radix UI and professional libraries
4. Backward Compatibility - Easy to migrate from uncontrolled to controlled
---
7. Types
Impact: HIGH
TypeScript patterns for component props including extending HTML attributes, exporting types, and single element wrapping.
7.1 Component Types
Impact: HIGH (Type-safe, flexible component interfaces)
Proper typing is essential for creating flexible, customizable, and type-safe component interfaces.
Each exported component should wrap a single HTML or JSX element:
Incorrect: hard to customize
const Card = ({ title, description, footer, ...props }) => (
<div {...props}>
<div className="card-header">
<h2>{title}</h2>
<p>{description}</p>
</div>
<div className="card-footer">{footer}</div>
</div>
);Correct: composable, each layer customizable
export type CardRootProps = React.ComponentProps<'div'> & {
variant?: 'default' | 'outlined';
};
export const CardRoot = ({ variant = 'default', ...props }: CardRootProps) => (
<div {...props} />
);Every component should extend native HTML attributes:
Common HTML Attribute Types:
// Enables type extraction
import type { CardRootProps } from '@/components/ui/card';
type Variant = CardRootProps['variant'];
// Enables extending
export type ExtendedCardProps = CardRootProps & { isLoading?: boolean };
// Enables wrapper components
const MyCard = (props: CardRootProps) => (
<CardRoot {...props} className={cn('my-custom-class', props.className)} />
);Always export prop types for consumers:
Naming convention: Export types as <ComponentName>Props.
1. Always Spread Props Last:
// ✅ User props override defaults
<div className="default-class" {...props} />
// ❌ Defaults override user props
<div {...props} className="default-class" />2. Avoid Prop Name Conflicts:
// ❌ Conflicts with HTML title attribute
type CardProps = React.ComponentProps<'div'> & { title: string };
// ✅ Use a different name
type CardProps = React.ComponentProps<'div'> & { heading: string };3. Document Custom Props:
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
} & React.ComponentPropsWithoutRef<E>;
function Component<E extends React.ElementType = 'div'>({
as,
...props
}: PolymorphicProps<E>) {
const Element = as || 'div';
return <Element {...props} />;
}For components that can render as different elements:
| Pattern | Usage | Example |
|---------|-------|---------|
| Basic extension | Extend single HTML element | React.ComponentProps<'div'> |
| Custom props | Add component-specific props | React.ComponentProps<'button'> & { variant?: string } |
| Polymorphic | Render as different elements | PolymorphicProps<T> |
| Type extraction | Get specific prop type | CardProps['variant'] |
| Type extension | Extend existing component props | CardProps & { isLoading?: boolean } |
---
8. Polymorphism
Impact: MEDIUM
Implementing the as prop pattern to change rendered HTML elements while preserving component functionality.
8.1 Polymorphism Patterns
Impact: MEDIUM (Flexible element rendering)
Build flexible components that can render as different HTML elements or components while maintaining consistent styling and behavior.
The as prop allows components to change their rendered element type:
Typography Components:
function Text({ as: Element = 'span', variant = 'body', ...props }) {
const className = cn(
'text-base',
variant === 'heading' && 'text-2xl font-bold',
variant === 'body' && 'text-base',
props.className
);
return <Element className={className} {...props} />;
}
<Text as="h1" variant="heading">Title</Text>
<Text as="p" variant="body">Paragraph</Text>Layout Components:
function Flex({ as: Element = 'div', ...props }) {
return <Element className={cn('flex', props.className)} {...props} />;
}
<Flex as="header" className="justify-between"><Logo /><Navigation /></Flex>
<Flex as="main" className="flex-col"><Content /></Flex>Interactive Elements:
function Clickable({ as: Element = 'button', ...props }) {
const isButton = Element === 'button';
const isAnchor = Element === 'a';
return (
<Element
role={!isButton && !isAnchor ? 'button' : undefined}
tabIndex={!isButton && !isAnchor ? 0 : undefined}
{...props}
/>
);
}1. Default to Semantic Elements:
Incorrect: too generic
function Component({ as: Element = 'div', ...props }) { }Correct: semantic defaults
function Article({ as: Element = 'article', ...props }) { }
function Navigation({ as: Element = 'nav', ...props }) { }
function Heading({ as: Element = 'h2', ...props }) { }2. Document Valid Elements:
interface BoxProps {
/**
* The HTML element to render as
* @default 'div'
*/
as?: 'div' | 'section' | 'article' | 'aside' | 'main' | 'header' | 'footer';
}3. Handle Event Handlers:
function Interactive({ as: Element = 'button', onClick, ...props }) {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (Element !== 'button' && (e.key === 'Enter' || e.key === ' ')) {
onClick?.(e as any);
}
};
return (
<Element
onClick={onClick}
onKeyDown={Element !== 'button' ? handleKeyDown : undefined}
{...props}
/>
);
}Invalid HTML Nesting:
// Incorrect: Invalid - button inside button
<Button as="button"><Button as="button">Nested</Button></Button>
// Incorrect: Invalid - div inside p
<Text as="p"><Box as="div">Invalid</Box></Text>
// Correct: Valid nesting
<Text as="div"><Box as="div">Valid</Box></Text>Missing Accessibility:
// ❌ Missing accessibility
<Box as="nav"><MenuItems /></Box>
// ✅ Proper accessibility
<Box as="nav" aria-label="Main navigation"><MenuItems /></Box>Performance:
// ❌ Creates new component on every render
function Parent() {
const CustomDiv = (props) => <div {...props} />;
return <Component as={CustomDiv} />;
}
// ✅ Stable component reference
const CustomDiv = (props) => <div {...props} />;
function Parent() {
return <Component as={CustomDiv} />;
}1. Semantic HTML Flexibility - Use the most appropriate element
2. Component Reusability - One component serves multiple purposes
3. Accessibility - Choose elements for best accessibility
4. Style System Integration - Maintain styling while changing elements
---
9. As-Child
Impact: MEDIUM
Radix UI Slot pattern for merging props and behaviors with custom child elements without wrapper elements.
9.1 asChild Pattern
Impact: MEDIUM (Prop merging without wrapper elements)
The asChild prop allows components to merge their props, behaviors, and event handlers with a custom child element instead of rendering a default DOM element.
Implement asChild when:
- Building trigger components (Dialog.Trigger, Tooltip.Trigger)
- Creating composable components that wrap other elements
- Allowing users to customize rendered element while preserving functionality
- Integrating with existing design system components
Without asChild: nested elements
<Dialog.Trigger>
<button>Open Dialog</button>
</Dialog.Trigger>
// Renders: <button><button>Open Dialog</button></button>With asChild: props merge onto child
<Dialog.Trigger asChild>
<button>Open Dialog</button>
</Dialog.Trigger>
// Renders: <button data-state="closed">Open Dialog</button>Custom Trigger Elements:
<AlertDialog.Trigger asChild>
<a href="/delete">Delete Account</a>
</AlertDialog.Trigger>
<Tooltip.Trigger asChild>
<IconButton icon={<InfoIcon />} />
</Tooltip.Trigger>
<DropdownMenu.Trigger asChild>
<Button variant="outline" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenu.Trigger>Semantic HTML:
<NavigationMenu.Link asChild>
<Link href="/products" className="nav-link">Products</Link>
</NavigationMenu.Link>Component Composition:
<Dialog.Trigger asChild>
<Tooltip.Trigger asChild>
<button>Open dialog (with tooltip)</button>
</Tooltip.Trigger>
</Dialog.Trigger>1. Child Must Spread Props:
Incorrect:
// Won't receive trigger behavior
const BadButton = ({ children }) => <button>{children}</button>;Correct:
// Properly receives all props
const GoodButton = ({ children, ...props }) => (
<button {...props}>{children}</button>
);2. Single Child Element Only:
Incorrect:
// Multiple children
<Trigger asChild>
<button>One</button>
<button>Two</button>
</Trigger>Correct:
// Single child
<Trigger asChild>
<button>Single Button</button>
</Trigger>3. Maintain Accessibility:
Correct:
// Maintains button semantics
<Dialog.Trigger asChild>
<button type="button">Open</button>
</Dialog.Trigger>Note: ensure proper ARIA
// Ensure proper ARIA when using non-semantic elements
<Dialog.Trigger asChild>
<div role="button" tabIndex={0} aria-label="Open dialog">Open</div>
</Dialog.Trigger>Don't manually clone elements:
Incorrect:
// Manual cloning misses edge cases
if (asChild) {
return React.cloneElement(children, { ...props });
}Correct:
// Use Slot for proper merging
if (asChild) {
return <Slot {...props}>{children}</Slot>;
}The asChild pattern is essential for:
- Composability - Enables flexible component composition
- Customizable - Allows users to control rendered elements
- Transparent - No hidden wrapper elements in DOM
- Accessible - Maintains semantic HTML structure
Reference: https://www.radix-ui.com/primitives/docs/utilities/slot
---
10. Data Attributes
Impact: LOW
Using data-state and data-slot attributes for styling component states and targeting sub-components.
10.1 Data Attributes for Styling
Impact: LOW (Clean state-based styling patterns)
Use data-state and data-slot attributes to create flexible, maintainable component APIs.
Never expose separate className props for different states. Use data-state attributes:**
const Dialog = ({ className, ...props }: DialogProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div
data-state={isOpen ? 'open' : 'closed'}
className={cn('transition-all', className)}
{...props}
/>
);
};Consumer styles from outside:
<form className="[&_[data-slot=button]]:w-full">
<Button>Submit</Button>
</form>Give components stable identifiers for parent targeting:
Incorrect:
data-slot="input" // Too generic
data-slot="blueButton" // Includes styling
data-slot="div-wrapper" // Implementation detailCorrect:
const Button = ({ variant = 'primary', size = 'md', loading, disabled, className, ...props }: ButtonProps) => {
return (
<button
data-slot="button"
data-loading={loading}
data-disabled={disabled}
className={cn(buttonVariants({ variant, size }), className)}
disabled={disabled}
{...props}
/>
);
};
// Usage
<form className="[&_[data-slot=button]]:w-full">
<Button loading={isLoading} className="data-[loading=true]:opacity-50">Submit</Button>
</form>| Pattern | Use For |
|---------|---------|
| data-state | Visual states (open/closed, active, loading) |
| data-slot | Component identification, parent-child targeting |
| props | Variants (primary, secondary), sizes, event handlers |
1. Use `data-state` instead of separate className props for states
2. Add `data-slot` to reusable components for targeting
3. Use kebab-case for data-slot values
4. Prefer Tailwind arbitrary variants over custom CSS
5. Never rely on class names for parent-child targeting
---
11. Styling
Impact: HIGH
Component styling with Tailwind CSS, cn utility, class-variance-authority (CVA), and intelligent class merging.
11.1 Component Styling with Tailwind CSS
Impact: HIGH (Predictable, maintainable styling patterns)
Use Tailwind CSS with intelligent class merging (tailwind-merge), conditional classes (clsx), and variant APIs (CVA).
Why: Without tailwind-merge, conflicting classes both apply. The cn utility resolves conflicts intelligently.
Incorrect:
// Without tailwind-merge, conflicting classes both apply
className="bg-red-500 bg-blue-500" // Both classes apply, causing conflicts
className="px-4 py-2 px-8" // Both px-4 and px-8 applyCorrect:
twMerge('bg-red-500', 'bg-blue-500'); // "bg-blue-500"
twMerge('px-4 py-2', 'px-8'); // "py-2 px-8"
twMerge('text-sm', 'text-lg'); // "text-lg"Apply classes in this order:
1. Base styles (always applied)
2. Variant styles (based on props)
3. Conditional styles (based on state)
4. User overrides (className prop)
Incorrect:
// Wrong order: user className comes before variants, preventing overrides
className={cn(className, variant === 'primary' && 'bg-blue-500')}Correct:
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-all",
{
variants: {
variant: {
default: "bg-blue-500 text-white hover:bg-blue-600",
destructive: "bg-red-500 text-white hover:bg-red-600",
outline: "border border-gray-300 bg-transparent hover:bg-gray-50",
ghost: "hover:bg-gray-100",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3 text-sm",
lg: "h-10 px-6",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
type ButtonProps = React.ComponentProps<'button'> & VariantProps<typeof buttonVariants>;
export const Button = ({ className, variant, size, ...props }: ButtonProps) => {
return (
<button className={cn(buttonVariants({ variant, size }), className)} {...props} />
);
};For components with multiple variants:
Key points:
- Define CVA variants outside the component
- Use
VariantProps<typeof variants>for TypeScript types
- Always merge with
classNameprop usingcn
1. Extract Repeated Patterns:
export const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
export const disabled = 'disabled:pointer-events-none disabled:opacity-50';
className={cn(focusRing, disabled, className)}2. Use CSS Variables for Dynamic Values:
Incorrect:
// Dynamic class generation (not detected by Tailwind)
<div className={`bg-[${dynamicColor}]`} />Correct:
// CSS variables
<div className="bg-[var(--color)]" style={{ '--color': dynamicColor } as React.CSSProperties} />3. Document Variants:
type ButtonProps = {
/** The visual style @default "default" */
variant?: 'default' | 'destructive' | 'outline' | 'ghost';
/** The size @default "default" */
size?: 'sm' | 'default' | 'lg' | 'icon';
};State-Based Styling:
<div className={cn(
'transition-all',
isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
)} />CVA with Additional Conditionals:
<div className={cn(
baseVariants({ variant, size }),
isActive && 'ring-2 ring-blue-500',
isDisabled && 'opacity-50 cursor-not-allowed',
className
)} />---
12. Design Tokens
Impact: MEDIUM
CSS variables for theming, color schemes, and consistent design system values across components.
12.1 Design Tokens
Impact: MEDIUM (Flexible theming and consistency)
Use semantic design tokens instead of hardcoded colors. Design tokens separate what something is from how it looks.
Base Tokens:
--background- Main page background
--foreground- Primary text color
--muted- Subtle backgrounds
--muted-foreground- Text on muted
--border- Border colors
--ring- Focus ring
Semantic Tokens:
--primary/--primary-foreground
--secondary/--secondary-foreground
--destructive/--destructive-foreground
--accent/--accent-foreground
Correct: semantic tokens
<button className="bg-primary text-primary-foreground hover:bg-primary/90">
Click me
</button>Incorrect: hardcoded colors
// Before
<div className="bg-white text-gray-900 border-gray-200">Content</div>
// After
<div className="bg-background text-foreground border-border">Content</div>Override tokens in .dark class:
Components automatically adapt because they reference tokens.
Use oklch() for better color manipulation:
1. Never hardcode colors - Always use design tokens
2. Use semantic names - --primary, not --blue-600
3. Keep tokens minimal - Start with base set, add when needed
4. Document token purpose - Add comments
5. Test theme switching - Ensure all components work in light/dark
6. Maintain contrast - Ensure WCAG contrast ratios
---
13. Documentation
Impact: MEDIUM
Documenting components with JSDoc, usage examples, accessibility notes, and prop descriptions.
13.1 Component Documentation
Impact: MEDIUM (Adoption and developer experience)
Create documentation that makes components accessible and easy to use.
Brief introduction explaining what the component does:
Include live demos with code:
Clear, copy-paste ready instructions:
List key capabilities:
Show variants, states, and advanced usage:
Document all props:
Document a11y features:
Track versions:
Use Real-World Examples:
// ✅ Real-world
function UserProfile({ user }) {
return (
<Card>
<CardHeader><CardTitle>{user.name}</CardTitle></CardHeader>
<CardContent>
<Button onClick={() => editUser(user.id)}>Edit Profile</Button>
</CardContent>
</Card>
)
}
// ❌ Too abstract
<Button onClick={handleClick}>Button</Button>Include Troubleshooting:
## Troubleshooting
**Button not responding:**
- Ensure `onClick` handler is provided
- Check if `disabled` is set
- Verify no parent is capturing eventsLink Related Components:
## Related
- [`IconButton`](/components/icon-button)
- [`ButtonGroup`](/components/button-group)Make Examples Runnable:
// ✅ Complete, runnable
import { Button } from "@/components/ui/button"
import { useState } from "react"
export function CounterButton() {
const [count, setCount] = useState(0)
return <Button onClick={() => setCount(count + 1)}>Count: {count}</Button>
}- [ ] Clear overview
- [ ] Live demo
- [ ] Installation instructions
- [ ] Feature list
- [ ] Multiple examples
- [ ] Complete API reference
- [ ] Accessibility docs
- [ ] Changelog
- [ ] Troubleshooting
- [ ] Related components
- [ ] All examples runnable
---
14. Registry
Impact: LOW
Component registry structure and schema for distributing components via registries like shadcn/ui.
14.1 Component Registries
Impact: LOW (Source code distribution)
Registries distribute source code, not compiled packages. This enables true component ownership and customization.
Bad:
// Traditional npm - compiled dependency
import { Button } from 'some-ui-library';Good:
// Registry-based - source code in your project
import { Button } from '@/components/ui/button';Bad:
{
"name": "announcement",
"type": "registry:component",
"description": "A compound badge component"
// Missing dependencies, registryDependencies, files, and category
}Good:
{
"name": "announcement",
"type": "registry:component",
"description": "A compound badge component",
"dependencies": ["class-variance-authority", "lucide-react"],
"registryDependencies": ["badge"],
"files": [
{
"type": "registry:component",
"path": "announcement.tsx",
"content": "..."
}
],
"category": "ui"
}1. Create structure:
my-component/
├── public/
│ └── metric-card.json
└── vercel.json2. Configure headers (`vercel.json`):
Example:
{
"headers": [
{
"source": "/(.*).json",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" },
{ "key": "Content-Type", "value": "application/json" }
]
}
]
}3. Deploy:
Example:
vercel --prod4. Users install via:
Example:
npx shadcn@latest add https://your-project.vercel.app/metric-card.jsonExample:
{
"name": "metric-card",
"type": "registry:component",
"description": "Display metrics with icon and trend",
"dependencies": ["lucide-react"],
"registryDependencies": ["card"],
"files": [
{
"type": "registry:component",
"path": "metric-card.tsx",
"content": "import { Card } from '@/components/ui/card'\n\nexport function MetricCard({ title, value }) {\n return <Card><h3>{title}</h3><p>{value}</p></Card>\n}"
}
],
"category": "ui"
}| Aspect | Registry | npm |
|--------|----------|-----|
| Distribution | Source code | Compiled |
| Ownership | Full control | Dependency lock |
| Customization | Modify freely | Fork/override |
| Updates | Manual copy | npm update |
| Bundle size | Only what you use | Full package |
For Authors:
1. Document dependencies
2. Version components
3. Provide examples
4. Test compatibility
5. Use semantic naming
For Consumers:
1. Review source code
2. Check dependencies
3. Customize freely
4. Track updates
5. Test thoroughly
Use registries when:
- Sharing with community
- Users need source ownership
- Building for specific frameworks
- Quick distribution needed
Use npm when:
- Distributing compiled code
- Need version management
- Framework-agnostic libraries
- Complex build processes
---
15. NPM
Impact: LOW
Publishing components to npm including package.json configuration, exports, and module formats.
15.1 Publishing to NPM
Impact: LOW (Traditional package distribution)
Distribute components as npm packages for stable, versioned dependencies with centralized updates.
Choose npm when:
- Users need stable, versioned dependencies
- Centralized control over updates
- Automatic dependency resolution
- Users don't need source code access
Bad:
{
"name": "@acme/ui-components",
"version": "1.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}Missing exports field, pointing to source files instead of built dist, and including React in dependencies instead of peerDependencies.
Correct:
{
"name": "@acme/ui-components",
"version": "1.0.0",
"description": "Accessible React components",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./styles.css": "./dist/styles.css"
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"dependencies": {
"clsx": "^2.0.0",
"tailwind-merge": "^2.0.0"
}
}Key points:
- Use
exportsfor modern module resolution
- Include ESM (
module) and CommonJS (main)
- Specify
typesfor TypeScript
- Use
peerDependenciesfor React
- Limit published files with
filesarray
Critical: Document this requirement for Tailwind-based components:
Example:
@import "tailwindcss";
/* Users must add this to scan your package */
@source "../node_modules/@acme/ui-components";Example:
{
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"prepublishOnly": "npm run build"
}
}Steps:
1. Build (npm run build)
2. Verify dist contents
3. Update version (npm version patch|minor|major)
4. Publish (npm publish)
Source Code:
- Users cannot modify directly
- Bug fixes require package updates
- Customization limited to exposed API
Bundle Size:
- All components included
- Tree-shaking helps but imperfect
Customization:
- Work within exposed API only
- Forking required for deep changes
Example:
import { Button } from '@acme/ui-components'
// Pre-built, versioned code from node_modulesnpm when:
- Stable, versioned dependencies needed
- Centralized updates preferred
- Source access not required
Registry when:
- Source code access needed
- Customization beyond props important
- Copy-paste workflow preferred
Consider offering both to let developers choose.
---
16. Marketplaces
Impact: LOW
Distribution strategies for component marketplaces and third-party component ecosystems.
16.1 Component Marketplaces
Impact: LOW (Centralized discovery and distribution)
Marketplaces like 21st.dev combine registry accessibility with package repository discoverability.
For Publishing:
- Share without managing infrastructure
- Reach built-in audience
- Monetize your work
- Get community feedback
For Consuming:
npx shadcn@latest add https://21st.dev/r/haydenbleasel/dialog-stack- Curated discovery by category
- Quality assurance
- Unified tooling
Example:
1. Hosting - No infrastructure management
2. Unified CLI - Same installation pattern
3. Previews - Live demos and examples
4. Discovery - Search, categories, recommendations
Ensure components have:
- Comprehensive documentation
- Multiple demo variations
- Responsive design
- Cross-browser testing
- Accessibility compliance
- Production-ready code
Bad:
{
"name": "my-component",
"description": "A component",
"demos": []
}Incomplete documentation, no demos, minimal description - marketplace users can't evaluate quality.
Good:
{
"name": "dialog-stack",
"description": "A composable dialog system with stack management, keyboard navigation, and focus trapping",
"demos": [
{
"title": "Basic Dialog",
"description": "Simple modal dialog with close button",
"code": "..."
},
{
"title": "Nested Dialogs",
"description": "Multiple dialogs in a stack",
"code": "..."
},
{
"title": "Form Dialog",
"description": "Dialog with form validation",
"code": "..."
}
],
"documentation": {
"props": "...",
"examples": "...",
"accessibility": "..."
}
}Comprehensive documentation, multiple demo variations, clear descriptions - enables proper evaluation.
For Authors:
- Distribution without infrastructure
- Built-in audience and discovery
- Monetization opportunities
- Community feedback (ratings, comments)
For Consumers:
- Curated discovery by category
- Quality assurance through reviews
- Unified installation tooling
For Authors:
- Competition and visibility
- Platform dependency
- Quality pressure
For Consumers:
- Variable quality despite reviews
- Lock-in concerns
- Discovery paradox (too much choice)
When Publishing:
1. Only publish production-ready components
2. Document thoroughly
3. Create multiple demos
4. Test extensively
5. Maintain actively
6. Engage with community
When Consuming:
Browse by category:
├── Marketing (Heroes, Pricing, Testimonials)
├── Application (Dashboards, Forms, Data Display)
└── E-commerce (Product Cards, Cart, Checkout)1. Evaluate before installing
2. Test in your environment
3. Check maintenance status
4. Review code quality
5. Consider alternatives
Always check:
- Component age and last update
- Documentation quality
- Author reputation
- Dependencies and compatibility
- Test in your environment
---
References
1. https://components.build 2. https://www.radix-ui.com/primitives 3. https://ui.shadcn.com
{
"version": "1.0.0",
"organization": "components.build",
"date": "January 2026",
"abstract": "Comprehensive guidelines for building modern, composable, and accessible UI components. This specification provides patterns and best practices for creating React components that are composable, accessible, customizable, and well-documented.",
"references": [
"https://components.build",
"https://www.radix-ui.com/primitives",
"https://ui.shadcn.com"
]
}
Components Build Skill
An agent skill for building modern, composable, and accessible React UI components following the components.build specification. Co-authored by Hayden Bleasel and shadcn.
Overview
This skill provides comprehensive guidelines across 16 categories covering everything from core principles to component distribution. It teaches agents how to build components that are:
- Composable - Break complex components into sub-components
- Accessible - Keyboard navigation, screen readers, ARIA
- Customizable - Extend HTML attributes, support theming
- Type-safe - TypeScript patterns for props and interfaces
Structure
components-build/
├── SKILL.md # Skill definition (loaded by agents)
├── AGENTS.md # Compiled rules (generated)
├── metadata.json # Version, organization, references
├── README.md # This file
└── rules/ # Individual rule files
├── _sections.md # Section metadata and ordering
├── _template.md # Template for new rules
├── accessibility.md # Individual rule files...
├── composition.md
├── styling.md
└── ...Rule Categories
| # | Category | Impact | Description |
|---|---|---|---|
| 1 | Overview | MEDIUM | Specification scope, goals, and philosophy |
| 2 | Principles | HIGH | Core design philosophy (composability, accessibility, etc.) |
| 3 | Definitions | MEDIUM | Common terminology (primitive, compound, headless) |
| 4 | Composition | HIGH | Breaking down complex components |
| 5 | Accessibility | CRITICAL | Keyboard, screen readers, ARIA, focus management |
| 6 | State | HIGH | Controlled/uncontrolled patterns |
| 7 | Types | HIGH | TypeScript props and interfaces |
| 8 | Polymorphism | MEDIUM | as prop for element switching |
| 9 | As-Child | MEDIUM | Radix Slot composition pattern |
| 10 | Data Attributes | LOW | data-state and data-slot patterns |
| 11 | Styling | HIGH | Tailwind CSS, cn utility, CVA |
| 12 | Design Tokens | MEDIUM | CSS variables and theming |
| 13 | Documentation | MEDIUM | JSDoc and usage examples |
| 14 | Registry | LOW | Component registry structure |
| 15 | NPM | LOW | Publishing to npm |
| 16 | Marketplaces | LOW | Distribution strategies |
Installation
Claude Code
cp -r skills/components-build ~/.claude/skills/Cursor
Copy to your Cursor skills directory:
cp -r skills/components-build ~/.cursor/skills/claude.ai
Add SKILL.md to your project knowledge, or paste its contents into a conversation.
Creating a New Rule
1. Copy rules/_template.md to rules/{category}.md 2. Use the appropriate category prefix from _sections.md 3. Fill in the frontmatter:
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description
tags: tag1, tag2
---4. Include clear Incorrect/Correct code examples 5. Add explanatory text and references
Rule File Structure
---
title: Use Semantic HTML Elements
impact: HIGH
tags: accessibility, html
---
## Use Semantic HTML Elements
Brief explanation of why this matters.
**Incorrect (using generic div):**
\`\`\`tsx
<div onClick={handleClick}>Click me</div>
\`\`\`
**Correct (using button element):**
\`\`\`tsx
<button onClick={handleClick}>Click me</button>
\`\`\`
Additional best practices.
Reference: [MDN Semantic HTML](https://developer.mozilla.org/en-US/docs/Glossary/Semantics)Impact Levels
- CRITICAL - Must implement, major accessibility or UX impact
- HIGH - Significant benefits, strongly recommended
- MEDIUM - Moderate improvements, recommended
- LOW - Nice to have, incremental improvements
Key Principles
1. Composition over Configuration - Break components into composable sub-components 2. Accessibility by Default - Not an afterthought, but a requirement 3. Single Element Wrapping - Each component wraps one HTML element 4. Extend HTML Attributes - Always extend native element props 5. Export Types - Make prop types available to consumers 6. Support Both State Patterns - Controlled and uncontrolled 7. Intelligent Class Merging - Use cn() utility with tailwind-merge
References
- components.build - Original specification
- Radix UI Primitives - Headless component patterns
- shadcn/ui - Reference implementation
Authors
- Hayden Bleasel (@haydenbleasel)
- shadcn (@shadcn)
Adapted as an AI skill by:
- Jordan Gilliam (@nolansym)
License
MIT
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Overview (overview)
Impact: MEDIUM Description: Specification scope, goals, and philosophy. Introduction to the components.build standard for building modern UI components.
2. Principles (principles)
Impact: HIGH Description: Core design principles for component architecture including composability, accessibility, customization, performance, transparency, and DX.
3. Definitions (definitions)
Impact: MEDIUM Description: Common terminology and component type definitions including primitive, compound, headless, and styled components.
4. Composition (composition)
Impact: HIGH Description: Breaking down complex components into composable sub-components using Root, Trigger, Content, and other naming patterns.
5. Accessibility (accessibility)
Impact: CRITICAL Description: Building accessible components with keyboard navigation, screen reader support, ARIA attributes, and focus management.
6. State (state)
Impact: HIGH Description: Controlled and uncontrolled state patterns using useControllableState for flexible component APIs.
7. Types (types)
Impact: HIGH Description: TypeScript patterns for component props including extending HTML attributes, exporting types, and single element wrapping.
8. Polymorphism (polymorphism)
Impact: MEDIUM Description: Implementing the as prop pattern to change rendered HTML elements while preserving component functionality.
9. As-Child (as-child)
Impact: MEDIUM Description: Radix UI Slot pattern for merging props and behaviors with custom child elements without wrapper elements.
10. Data Attributes (data-attributes)
Impact: LOW Description: Using data-state and data-slot attributes for styling component states and targeting sub-components.
11. Styling (styling)
Impact: HIGH Description: Component styling with Tailwind CSS, cn utility, class-variance-authority (CVA), and intelligent class merging.
12. Design Tokens (design-tokens)
Impact: MEDIUM Description: CSS variables for theming, color schemes, and consistent design system values across components.
13. Documentation (documentation)
Impact: MEDIUM Description: Documenting components with JSDoc, usage examples, accessibility notes, and prop descriptions.
14. Registry (registry)
Impact: LOW Description: Component registry structure and schema for distributing components via registries like shadcn/ui.
15. NPM (npm)
Impact: LOW Description: Publishing components to npm including package.json configuration, exports, and module formats.
16. Marketplaces (marketplaces)
Impact: LOW Description: Distribution strategies for component marketplaces and third-party component ecosystems.
Rule Title Here
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the benefits and use cases.
Incorrect (description of what's wrong):
// Bad code example here
const bad = example()Correct (description of what's right):
// Good code example here
const good = example()Additional explanatory text and best practices.
Reference: Link to documentation or resource
Accessibility Guidelines
Accessibility (a11y) is not optional—it's a fundamental requirement. Every component must be usable by everyone, including people with visual, motor, auditory, or cognitive disabilities.
Core Principles
1. Semantic HTML First
Always start with the most appropriate HTML element:
Incorrect (generic div):
<div onClick={handleClick} className="button">Click me</div>Correct (semantic element):
<button onClick={handleClick}>Click me</button>2. Keyboard Navigation
Every interactive element must be keyboard accessible:
function Menu() {
const handleKeyDown = (e: React.KeyboardEvent) => {
switch(e.key) {
case 'ArrowDown': focusNextItem(); break;
case 'ArrowUp': focusPreviousItem(); break;
case 'Home': focusFirstItem(); break;
case 'End': focusLastItem(); break;
case 'Escape': closeMenu(); break;
}
};
return <div role="menu" onKeyDown={handleKeyDown}>{/* items */}</div>;
}3. Screen Reader Support
Use ARIA attributes when necessary:
<nav aria-label="Main navigation">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
</ul>
</nav>
<div aria-live="polite" aria-atomic="true">
{isLoading && <span>Loading results...</span>}
</div>4. Visual Accessibility
Support users with visual impairments:
/* Visible focus indicators */
button:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
/* Sufficient color contrast (4.5:1 for normal text) */
.text { color: #333; background: white; }ARIA Rules
1. Don't use ARIA if you can use semantic HTML 2. Don't change native semantics unless necessary 3. All interactive elements must be keyboard accessible 4. Don't hide focusable elements - Never use aria-hidden="true" on focusable elements 5. All interactive elements must have accessible names
Component Patterns
Modal/Dialog
function Modal({ isOpen, onClose, children }) {
return isOpen ? (
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<button onClick={onClose} aria-label="Close dialog">×</button>
{children}
</div>
) : null;
}Dropdown Menu
<button aria-haspopup="true" aria-expanded={isOpen} aria-controls="dropdown-menu">
Menu
</button>
{isOpen && (
<ul id="dropdown-menu" role="menu">
<li role="menuitem" tabIndex={-1}>Item 1</li>
</ul>
)}Tabs
<div role="tablist" aria-label="Tabs">
<button role="tab" aria-selected={activeTab === 0} aria-controls="panel-0">Tab 1</button>
</div>
<div id="panel-0" role="tabpanel" aria-labelledby="tab-0">{/* content */}</div>Focus Management
/* Show outline only for keyboard focus */
*:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}Live Regions
// Polite announcement
<div role="status" aria-live="polite">{savedMessage && "Saved"}</div>
// Assertive announcement
<div role="alert" aria-live="assertive">{errorMessage}</div>Common Pitfalls
Placeholder as labels:
// ❌ Placeholder disappears
<input placeholder="Email address" />
// ✅ Persistent label
<label>Email address <input type="email" /></label>Empty buttons:
// ❌ No accessible name
<button><TrashIcon /></button>
// ✅ Screen reader text
<button aria-label="Delete item"><TrashIcon aria-hidden="true" /></button>Disabled elements:
// ✅ Use aria-disabled and explain
<button
aria-disabled={!isValid}
aria-describedby="submit-help"
onClick={isValid ? handleSubmit : undefined}
>Submit</button>
<span id="submit-help">{!isValid && 'Fill required fields'}</span>Mobile Accessibility
/* Minimum 44x44px touch targets */
.button {
min-height: 44px;
min-width: 44px;
}<!-- Allow zooming -->
<meta name="viewport" content="width=device-width, initial-scale=1">asChild Pattern
The asChild prop allows components to merge their props, behaviors, and event handlers with a custom child element instead of rendering a default DOM element.
When to Use
Implement asChild when:
- Building trigger components (Dialog.Trigger, Tooltip.Trigger)
- Creating composable components that wrap other elements
- Allowing users to customize rendered element while preserving functionality
- Integrating with existing design system components
Implementation
import { Slot } from '@radix-ui/react-slot';
interface TriggerProps extends React.ComponentPropsWithoutRef<'button'> {
asChild?: boolean;
}
export const Trigger = React.forwardRef<HTMLButtonElement, TriggerProps>(
({ asChild = false, children, ...props }, ref) => {
if (asChild) {
return <Slot ref={ref} {...props}>{children}</Slot>;
}
return <button ref={ref} {...props}>{children}</button>;
}
);How It Works
Without asChild (nested elements):
<Dialog.Trigger>
<button>Open Dialog</button>
</Dialog.Trigger>
// Renders: <button><button>Open Dialog</button></button>With asChild (props merge onto child):
<Dialog.Trigger asChild>
<button>Open Dialog</button>
</Dialog.Trigger>
// Renders: <button data-state="closed">Open Dialog</button>Common Use Cases
Custom Trigger Elements:
<AlertDialog.Trigger asChild>
<a href="/delete">Delete Account</a>
</AlertDialog.Trigger>
<Tooltip.Trigger asChild>
<IconButton icon={<InfoIcon />} />
</Tooltip.Trigger>
<DropdownMenu.Trigger asChild>
<Button variant="outline" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenu.Trigger>Semantic HTML:
<NavigationMenu.Link asChild>
<Link href="/products" className="nav-link">Products</Link>
</NavigationMenu.Link>Component Composition:
<Dialog.Trigger asChild>
<Tooltip.Trigger asChild>
<button>Open dialog (with tooltip)</button>
</Tooltip.Trigger>
</Dialog.Trigger>Best Practices
1. Child Must Spread Props:
Incorrect:
// Won't receive trigger behavior
const BadButton = ({ children }) => <button>{children}</button>;Correct:
// Properly receives all props
const GoodButton = ({ children, ...props }) => (
<button {...props}>{children}</button>
);2. Single Child Element Only:
Incorrect:
// Multiple children
<Trigger asChild>
<button>One</button>
<button>Two</button>
</Trigger>Correct:
// Single child
<Trigger asChild>
<button>Single Button</button>
</Trigger>3. Maintain Accessibility:
Correct:
// Maintains button semantics
<Dialog.Trigger asChild>
<button type="button">Open</button>
</Dialog.Trigger>Note (ensure proper ARIA):
// Ensure proper ARIA when using non-semantic elements
<Dialog.Trigger asChild>
<div role="button" tabIndex={0} aria-label="Open dialog">Open</div>
</Dialog.Trigger>Common Pitfalls
Don't manually clone elements:
Incorrect:
// Manual cloning misses edge cases
if (asChild) {
return React.cloneElement(children, { ...props });
}Correct:
// Use Slot for proper merging
if (asChild) {
return <Slot {...props}>{children}</Slot>;
}Integration
The asChild pattern is essential for:
- Composability - Enables flexible component composition
- Customizable - Allows users to control rendered elements
- Transparent - No hidden wrapper elements in DOM
- Accessible - Maintains semantic HTML structure
Reference: @radix-ui/react-slot
Component Composition
Composition is the foundation of building modern UI components. Instead of cramming all functionality into a single component with dozens of props, break components down into smaller, focused sub-components that work together.
Incorrect (monolithic, hard to customize):
<Accordion data={data} />Correct (composable, each layer customizable):
<Accordion.Root open={open} setOpen={setOpen}>
{data.map((item) => (
<Accordion.Item key={item.title}>
<Accordion.Trigger>{item.title}</Accordion.Trigger>
<Accordion.Content>{item.content}</Accordion.Content>
</Accordion.Item>
))}
</Accordion.Root>Building Composable Components
1. Root Component
The main container that holds sub-components and manages shared state using Context:
const AccordionContext = createContext<AccordionContextValue>({
open: false,
setOpen: () => {},
});
export const Root = ({ children, open, setOpen, ...props }: AccordionRootProps) => (
<AccordionContext.Provider value={{ open, setOpen }}>
<div {...props}>{children}</div>
</AccordionContext.Provider>
);2. Item Component
A simple wrapper for each item:
export const Item = (props: AccordionItemProps) => <div {...props} />;3. Trigger Component
Handles user interaction:
export const Trigger = ({ asChild, ...props }: AccordionTriggerProps) => {
const { open, setOpen } = useContext(AccordionContext);
return (
<button onClick={() => setOpen(!open)} aria-expanded={open} {...props} />
);
};4. Content Component
Displays the main content:
export const Content = ({ asChild, ...props }: AccordionContentProps) => {
const { open } = useContext(AccordionContext);
if (!open) return null;
return <div {...props} />;
};5. Export Pattern
Export components as a namespace:
export const Accordion = { Root, Item, Trigger, Content };
// Usage
<Accordion.Root open={open} setOpen={setOpen}>
<Accordion.Item>
<Accordion.Trigger>Title</Accordion.Trigger>
<Accordion.Content>Content</Accordion.Content>
</Accordion.Item>
</Accordion.Root>Naming Conventions
- Root - Main container component
- Trigger - Element that initiates an action
- Content - Element containing the main content
- Header/Body/Footer - Structured content areas
- Title/Description - Informational components
Best Practices
1. Single Responsibility - Each sub-component should have one clear purpose 2. Context for State - Use React Context to share state between sub-components 3. Extend HTML Attributes - Always extend native HTML element props 4. Consistent Naming - Follow established conventions 5. Namespace Exports - Export components as a namespace for clean API 6. Composition Over Configuration - Prefer multiple components over many props
When to Use Composition
Use composition when:
- A component has multiple responsibilities
- Customization requires CSS overrides
- You need flexible layouts or structures
- Building a component library
- Components need to work together but remain independent
Avoid composition for:
- Simple, single-purpose components
- Components that don't need customization
- Over-engineering simple UI elements
Data Attributes for Component Styling
Use data-state and data-slot attributes to create flexible, maintainable component APIs.
Use data-state for Visual States
Never expose separate className props for different states. Use data-state attributes:
const Dialog = ({ className, ...props }: DialogProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div
data-state={isOpen ? 'open' : 'closed'}
className={cn('transition-all', className)}
{...props}
/>
);
};Consumer styles from outside:
<Dialog className="data-[state=open]:opacity-100 data-[state=closed]:opacity-0" />Common State Patterns
// Open/closed
<Accordion data-state={isOpen ? 'open' : 'closed'} />
// Selected
<Tab data-state={isSelected ? 'active' : 'inactive'} />
// Disabled
<Button data-disabled={isDisabled} disabled={isDisabled} />
// Loading
<Button data-loading={isLoading} />
// Orientation
<Slider data-orientation="horizontal" />
// Side/position
<Tooltip data-side="top" />Tailwind Integration
<Dialog
className={cn(
'rounded-lg border p-4',
'data-[state=open]:animate-in data-[state=open]:fade-in',
'data-[state=closed]:animate-out data-[state=closed]:fade-out'
)}
/>Use data-slot for Component Identification
Give components stable identifiers for parent targeting:
function CheckboxGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="checkbox-group"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
}
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3",
className
)}
{...props}
/>
);
}Parent-Aware Styling
<form className="[&_[data-slot=button]]:w-full">
<Button>Submit</Button>
</form>Naming Conventions
Incorrect:
data-slot="input" // Too generic
data-slot="blueButton" // Includes styling
data-slot="div-wrapper" // Implementation detailCorrect:
data-slot="search-input"
data-slot="navigation-menu"
data-slot="error-message"
data-slot="submit-button"When to Use Each
| Pattern | Use For |
|---|---|
data-state | Visual states (open/closed, active, loading) |
data-slot | Component identification, parent-child targeting |
props | Variants (primary, secondary), sizes, event handlers |
Combined Example
const Button = ({ variant = 'primary', size = 'md', loading, disabled, className, ...props }: ButtonProps) => {
return (
<button
data-slot="button"
data-loading={loading}
data-disabled={disabled}
className={cn(buttonVariants({ variant, size }), className)}
disabled={disabled}
{...props}
/>
);
};
// Usage
<form className="[&_[data-slot=button]]:w-full">
<Button loading={isLoading} className="data-[loading=true]:opacity-50">Submit</Button>
</form>Rules
1. Use `data-state` instead of separate className props for states 2. Add `data-slot` to reusable components for targeting 3. Use kebab-case for data-slot values 4. Prefer Tailwind arbitrary variants over custom CSS 5. Never rely on class names for parent-child targeting
Component Artifact Definitions
Precise terminology for classifying and naming UI artifacts.
Primitive (Unstyled Component)
The lowest-level building block providing behavior and accessibility without styling. Completely headless.
// Primitive - behavior only, no styling
function DialogPrimitive({ open, onOpenChange, children }) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
{children}
</Dialog.Root>
);
}Examples: Radix UI Primitives, React Aria Components, Headless UI
Component
A styled, reusable UI unit that adds visual design to primitives or composes multiple elements.
// Component - styled and reusable
function Button({ variant = 'primary', size = 'md', children, ...props }) {
return (
<button className={cn('base-button-styles', variants[variant], sizes[size])} {...props}>
{children}
</button>
);
}Examples: shadcn/ui components, Material UI, Ant Design
Pattern
A specific composition solving a UI/UX problem. Documentation-focused, not a reusable component.
Examples: Form validation with inline errors, confirming destructive actions, typeahead search
Block
An opinionated, production-ready composition solving a concrete interface use case.
// Block - complete, opinionated composition
function PricingTable({ plans, onSelectPlan }) {
return (
<div className="pricing-table">
{plans.map(plan => (
<Card key={plan.id}>
<Card.Header><Card.Title>{plan.name}</Card.Title></Card.Header>
<Card.Content><div className="price">{plan.price}</div></Card.Content>
<Card.Footer><Button onClick={() => onSelectPlan(plan.id)}>Select Plan</Button></Card.Footer>
</Card>
))}
</div>
);
}Examples: Pricing table, auth screens, onboarding stepper, AI chat panel
Page
A complete, single-route view composed of multiple blocks.
function LandingPage() {
return (
<Layout>
<HeroBlock />
<FeaturesBlock />
<PricingBlock />
<FooterBlock />
</Layout>
);
}Template
A multi-page collection or full-site scaffold bundling pages, routing, layouts, and providers.
Examples: SaaS starter, e-commerce template, dashboard starter
Utility (Non-visual)
A helper for developer ergonomics or composition; not rendered UI.
function useControllableState({ prop, defaultProp, onChange }) {
const [state, setState] = useState(defaultProp);
const value = prop !== undefined ? prop : state;
const setValue = (newValue) => {
if (prop === undefined) setState(newValue);
onChange?.(newValue);
};
return [value, setValue];
}Examples: React hooks, class utilities, keybinding helpers, focus scopes
Classification Decision Flow
1. Behavior/a11y only, no styling? → Primitive 2. Styled, reusable UI element? → Component 3. Concrete product use case with opinionated composition? → Block 4. Multi-page scaffold with routing/providers? → Template 5. Documentation of recurring solution? → Pattern 6. Non-visual logic? → Utility
Common Classification Mistakes
Incorrect (mislabeling a styled component as a primitive):
// Wrong: This is styled, so it's a Component, not a Primitive
function DialogPrimitive({ open, onOpenChange, children }) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Content className="rounded-lg border bg-white p-6 shadow-lg">
{children}
</Dialog.Content>
</Dialog.Root>
);
}Correct (proper classification based on styling presence):
// Primitive - behavior only, no styling
function DialogPrimitive({ open, onOpenChange, children }) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
{children}
</Dialog.Root>
);
}
// Component - styled wrapper around primitive
function Dialog({ open, onOpenChange, children }) {
return (
<DialogPrimitive open={open} onOpenChange={onOpenChange}>
<Dialog.Content className={cn("rounded-lg border bg-white p-6 shadow-lg")}>
{children}
</Dialog.Content>
</DialogPrimitive>
);
}Key Vocabulary
- Props API - Public configuration surface, typed and documented
- Children/Slots - Placeholders for caller-provided structure
- Render Prop - Function child delegating rendering
- Controlled - Value driven by props (parent is source of truth)
- Uncontrolled - Internal state with optional
defaultValue - Provider/Context - Supplies shared state to subtree
- Portal - Rendering outside DOM hierarchy for layering
- Headless - Behavior without styling
- Styled - Ships with default visual design
- Variants - Style/behavior permutations via props
- Design Tokens - Named values for theming
Design Tokens
Use semantic design tokens instead of hardcoded colors. Design tokens separate what something is from how it looks.
Variable Architecture
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
}Token Naming
Base Tokens:
--background- Main page background--foreground- Primary text color--muted- Subtle backgrounds--muted-foreground- Text on muted--border- Border colors--ring- Focus ring
Semantic Tokens:
--primary/--primary-foreground--secondary/--secondary-foreground--destructive/--destructive-foreground--accent/--accent-foreground
Usage in Components
Correct (semantic tokens):
<button className="bg-primary text-primary-foreground hover:bg-primary/90">
Click me
</button>Incorrect (hardcoded colors):
<button className="bg-blue-600 text-white hover:bg-blue-700">
Click me
</button>Component Examples
// Card
<div className="bg-background border border-border rounded-lg p-6">
<h2 className="text-foreground font-semibold">Title</h2>
<p className="text-muted-foreground">Description</p>
</div>
// Button variants
<button className="bg-primary text-primary-foreground">Primary</button>
<button className="bg-secondary text-secondary-foreground">Secondary</button>
<button className="bg-destructive text-destructive-foreground">Delete</button>
// Input
<input className="bg-background border border-border text-foreground placeholder:text-muted-foreground" />Dark Mode
Override tokens in .dark class:
:root { --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); }
.dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); }Components automatically adapt because they reference tokens.
Color Format: OKLCH
Use oklch() for better color manipulation:
/* ✅ OKLCH - perceptually uniform */
--primary: oklch(0.5 0.2 250);
/* ⚠️ RGB/HSL - harder to manipulate */
--primary: rgb(100, 150, 200);Best Practices
1. Never hardcode colors - Always use design tokens 2. Use semantic names - --primary, not --blue-600 3. Keep tokens minimal - Start with base set, add when needed 4. Document token purpose - Add comments 5. Test theme switching - Ensure all components work in light/dark 6. Maintain contrast - Ensure WCAG contrast ratios
Theme Variants
:root { --primary: oklch(0.5 0.2 250); }
[data-theme="brand-b"] { --primary: oklch(0.5 0.2 120); }
[data-theme="brand-c"] { --primary: oklch(0.5 0.2 0); }Migration
// Before
<div className="bg-white text-gray-900 border-gray-200">Content</div>
// After
<div className="bg-background text-foreground border-border">Content</div>Component Documentation Guide
Create documentation that makes components accessible and easy to use.
Essential Sections
1. Overview
Brief introduction explaining what the component does:
# Button
A versatile button component with multiple variants and sizes. Use for primary actions,
secondary actions, or destructive operations.2. Demo and Source Code
Include live demos with code:
import { Button } from "@/components/ui/button"
export function ButtonDemo() {
return (
<div className="flex gap-2">
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
</div>
)
}3. Installation
Clear, copy-paste ready instructions:
# shadcn/ui CLI
npx shadcn@latest add button
# npm
npm install @acme/ui-components4. Features
List key capabilities:
## Features
- **Customizable** – Adjust styles, sizes, and behavior
- **Accessible** – Keyboard navigation, ARIA, screen reader support
- **Composable** – Works with other components
- **Type-safe** – Comprehensive TypeScript types
- **Theming** – Integrates with design tokens5. Examples
Show variants, states, and advanced usage:
// Variants
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
// States
<Button disabled>Disabled</Button>
<Button isLoading>Loading...</Button>
// With icon
<Button><Icon /> Save</Button>6. Props/API Reference
Document all props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `"default" \| "secondary" \| "destructive"` | `"default"` | Visual style |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size |
| `disabled` | `boolean` | `false` | Disabled state |
| `onClick` | `(event: MouseEvent) => void` | - | Click handler |7. Accessibility
Document a11y features:
## Accessibility
- **Keyboard** - Enter/Space activation
- **ARIA** - Proper roles and attributes
- **Focus** - Visible `:focus-visible` indicators
- **Contrast** - WCAG AA (4.5:1)8. Changelog
Track versions:
## Changelog
### v2.0.0
**Breaking:** `variant` uses `"default"` instead of `"primary"`
### v1.2.0
- Added `isLoading` prop
- Added `icon` propBest Practices
Use Real-World Examples:
// ✅ Real-world
function UserProfile({ user }) {
return (
<Card>
<CardHeader><CardTitle>{user.name}</CardTitle></CardHeader>
<CardContent>
<Button onClick={() => editUser(user.id)}>Edit Profile</Button>
</CardContent>
</Card>
)
}
// ❌ Too abstract
<Button onClick={handleClick}>Button</Button>Include Troubleshooting:
## Troubleshooting
**Button not responding:**
- Ensure `onClick` handler is provided
- Check if `disabled` is set
- Verify no parent is capturing eventsLink Related Components:
## Related
- [`IconButton`](/components/icon-button)
- [`ButtonGroup`](/components/button-group)Make Examples Runnable:
// ✅ Complete, runnable
import { Button } from "@/components/ui/button"
import { useState } from "react"
export function CounterButton() {
const [count, setCount] = useState(0)
return <Button onClick={() => setCount(count + 1)}>Count: {count}</Button>
}Documentation Checklist
- [ ] Clear overview
- [ ] Live demo
- [ ] Installation instructions
- [ ] Feature list
- [ ] Multiple examples
- [ ] Complete API reference
- [ ] Accessibility docs
- [ ] Changelog
- [ ] Troubleshooting
- [ ] Related components
- [ ] All examples runnable
Component Marketplaces
Marketplaces like 21st.dev combine registry accessibility with package repository discoverability.
When to Use
For Publishing:
- Share without managing infrastructure
- Reach built-in audience
- Monetize your work
- Get community feedback
For Consuming:
- Curated discovery by category
- Quality assurance
- Unified tooling
Installation Pattern
npx shadcn@latest add https://21st.dev/r/<author>/<component>Example:
npx shadcn@latest add https://21st.dev/r/haydenbleasel/dialog-stackWhat Marketplaces Provide
1. Hosting - No infrastructure management 2. Unified CLI - Same installation pattern 3. Previews - Live demos and examples 4. Discovery - Search, categories, recommendations
Publishing Requirements
Ensure components have:
- Comprehensive documentation
- Multiple demo variations
- Responsive design
- Cross-browser testing
- Accessibility compliance
- Production-ready code
Bad:
{
"name": "my-component",
"description": "A component",
"demos": []
}Incomplete documentation, no demos, minimal description - marketplace users can't evaluate quality.
Good:
{
"name": "dialog-stack",
"description": "A composable dialog system with stack management, keyboard navigation, and focus trapping",
"demos": [
{
"title": "Basic Dialog",
"description": "Simple modal dialog with close button",
"code": "..."
},
{
"title": "Nested Dialogs",
"description": "Multiple dialogs in a stack",
"code": "..."
},
{
"title": "Form Dialog",
"description": "Dialog with form validation",
"code": "..."
}
],
"documentation": {
"props": "...",
"examples": "...",
"accessibility": "..."
}
}Comprehensive documentation, multiple demo variations, clear descriptions - enables proper evaluation.
Benefits
For Authors:
- Distribution without infrastructure
- Built-in audience and discovery
- Monetization opportunities
- Community feedback (ratings, comments)
For Consumers:
- Curated discovery by category
- Quality assurance through reviews
- Unified installation tooling
Challenges
For Authors:
- Competition and visibility
- Platform dependency
- Quality pressure
For Consumers:
- Variable quality despite reviews
- Lock-in concerns
- Discovery paradox (too much choice)
Best Practices
When Publishing: 1. Only publish production-ready components 2. Document thoroughly 3. Create multiple demos 4. Test extensively 5. Maintain actively 6. Engage with community
When Consuming: 1. Evaluate before installing 2. Test in your environment 3. Check maintenance status 4. Review code quality 5. Consider alternatives
Discovery Categories
Browse by category:
├── Marketing (Heroes, Pricing, Testimonials)
├── Application (Dashboards, Forms, Data Display)
└── E-commerce (Product Cards, Cart, Checkout)Due Diligence
Always check:
- Component age and last update
- Documentation quality
- Author reputation
- Dependencies and compatibility
- Test in your environment
Publishing Components to NPM
Distribute components as npm packages for stable, versioned dependencies with centralized updates.
When to Use NPM
Choose npm when:
- Users need stable, versioned dependencies
- Centralized control over updates
- Automatic dependency resolution
- Users don't need source code access
Package Configuration
Bad:
{
"name": "@acme/ui-components",
"version": "1.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}Missing exports field, pointing to source files instead of built dist, and including React in dependencies instead of peerDependencies.
Correct:
{
"name": "@acme/ui-components",
"version": "1.0.0",
"description": "Accessible React components",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./styles.css": "./dist/styles.css"
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"dependencies": {
"clsx": "^2.0.0",
"tailwind-merge": "^2.0.0"
}
}Key points:
- Use
exportsfor modern module resolution - Include ESM (
module) and CommonJS (main) - Specify
typesfor TypeScript - Use
peerDependenciesfor React - Limit published files with
filesarray
Tailwind Configuration
Critical: Document this requirement for Tailwind-based components:
Example:
@import "tailwindcss";
/* Users must add this to scan your package */
@source "../node_modules/@acme/ui-components";Build and Publish
Example:
{
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"prepublishOnly": "npm run build"
}
}Steps: 1. Build (npm run build) 2. Verify dist contents 3. Update version (npm version patch|minor|major) 4. Publish (npm publish)
Trade-offs
Source Code:
- Users cannot modify directly
- Bug fixes require package updates
- Customization limited to exposed API
Bundle Size:
- All components included
- Tree-shaking helps but imperfect
Customization:
- Work within exposed API only
- Forking required for deep changes
Usage
Example:
npm install @acme/ui-componentsimport { Button } from '@acme/ui-components'
// Pre-built, versioned code from node_modulesChoosing Distribution
npm when:
- Stable, versioned dependencies needed
- Centralized updates preferred
- Source access not required
Registry when:
- Source code access needed
- Customization beyond props important
- Copy-paste workflow preferred
Consider offering both to let developers choose.
Components.build Overview
The components.build specification is an open-source standard for building modern, composable, and accessible UI components. It provides high-level guidelines, best practices, and common terminology for designing UI components that integrate smoothly into any codebase.
What This Specification Is:
This spec is not:
- A tutorial or course on React
- A promotion for any specific component library or registry
- A replacement for framework documentation
This spec is:
- A set of high-level guidelines and best practices
- A common terminology for designing UI components
- A standard for ensuring components meet modern expectations
- A framework for creating components that integrate smoothly across projects
Who This Is For:
This specification is written for:
- Open-source maintainers building and distributing component libraries
- Senior front-end engineers designing component APIs and design systems
- Developers familiar with JavaScript/TypeScript and React
Framework Scope:
While examples use React (with JSX/TSX) for concreteness, the fundamental concepts apply to other frameworks (Vue, Svelte, Angular). The philosophy is framework-agnostic.
Core Goals:
The specification aims to help developers create components that are:
1. Composable - Components combine and nest to create complex UIs 2. Accessible - Usable by everyone, including users with disabilities 3. Easy to adopt - Integrate smoothly into any codebase 4. Consistent - Follow modern expectations and patterns 5. Well-documented - Clear guidelines and terminology
Key Philosophy:
- Composition over configuration - Build flexible, composable APIs
- Accessibility by default - Not an afterthought, but a requirement
- Developer experience - Components should be easy to understand, customize, and integrate
- Transparency - Source code should be inspectable and modifiable
- Standards alignment - Follow web standards and modern best practices
Example:
The following examples illustrate the difference between components that don't follow the specification and those that do:
Incorrect:
// Hard-coded styles, no accessibility, not composable
function Button() {
return (
<button style={{ backgroundColor: '#007bff', color: 'white', padding: '10px' }}>
Click me
</button>
);
}Correct:
// Composable, accessible, customizable
import { cn } from '@/lib/utils';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'outline';
}
export function Button({
className,
variant = 'default',
children,
...props
}: ButtonProps) {
return (
<button
className={cn(
'inline-flex items-center justify-center rounded-md px-4 py-2',
'focus-visible:outline-none focus-visible:ring-2',
variant === 'default' && 'bg-primary text-primary-foreground',
variant === 'outline' && 'border border-input bg-background',
className
)}
{...props}
>
{children}
</button>
);
}Specification Authors:
Co-authored by:
- Hayden Bleasel (@haydenbleasel)
- shadcn (@shadcn)
Adapted as an AI skill by:
- Jordan Gilliam (@nolansym)
Reference: https://components.build
Polymorphism Patterns
Build flexible components that can render as different HTML elements or components while maintaining consistent styling and behavior.
Core Pattern: The as Prop
The as prop allows components to change their rendered element type:
<Button as="a" href="/home">Go Home</Button>
<Button as="button" type="submit">Submit</Button>
<Button as="div" role="button" tabIndex={0}>Custom Element</Button>Basic Implementation
function Component({ as: Element = 'div', children, ...props }) {
return <Element {...props}>{children}</Element>;
}TypeScript Implementation
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
children?: React.ReactNode;
} & React.ComponentPropsWithoutRef<E>;
function Component<E extends React.ElementType = 'div'>({
as,
children,
...props
}: PolymorphicProps<E>) {
const Element = as || 'div';
return <Element {...props}>{children}</Element>;
}Common Use Cases
Typography Components:
function Text({ as: Element = 'span', variant = 'body', ...props }) {
const className = cn(
'text-base',
variant === 'heading' && 'text-2xl font-bold',
variant === 'body' && 'text-base',
props.className
);
return <Element className={className} {...props} />;
}
<Text as="h1" variant="heading">Title</Text>
<Text as="p" variant="body">Paragraph</Text>Layout Components:
function Flex({ as: Element = 'div', ...props }) {
return <Element className={cn('flex', props.className)} {...props} />;
}
<Flex as="header" className="justify-between"><Logo /><Navigation /></Flex>
<Flex as="main" className="flex-col"><Content /></Flex>Interactive Elements:
function Clickable({ as: Element = 'button', ...props }) {
const isButton = Element === 'button';
const isAnchor = Element === 'a';
return (
<Element
role={!isButton && !isAnchor ? 'button' : undefined}
tabIndex={!isButton && !isAnchor ? 0 : undefined}
{...props}
/>
);
}Best Practices
1. Default to Semantic Elements:
Incorrect (too generic):
function Component({ as: Element = 'div', ...props }) { }Correct (semantic defaults):
function Article({ as: Element = 'article', ...props }) { }
function Navigation({ as: Element = 'nav', ...props }) { }
function Heading({ as: Element = 'h2', ...props }) { }2. Document Valid Elements:
interface BoxProps {
/**
* The HTML element to render as
* @default 'div'
*/
as?: 'div' | 'section' | 'article' | 'aside' | 'main' | 'header' | 'footer';
}3. Handle Event Handlers:
function Interactive({ as: Element = 'button', onClick, ...props }) {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (Element !== 'button' && (e.key === 'Enter' || e.key === ' ')) {
onClick?.(e as any);
}
};
return (
<Element
onClick={onClick}
onKeyDown={Element !== 'button' ? handleKeyDown : undefined}
{...props}
/>
);
}Common Pitfalls
Invalid HTML Nesting:
// Incorrect: Invalid - button inside button
<Button as="button"><Button as="button">Nested</Button></Button>
// Incorrect: Invalid - div inside p
<Text as="p"><Box as="div">Invalid</Box></Text>
// Correct: Valid nesting
<Text as="div"><Box as="div">Valid</Box></Text>Missing Accessibility:
// ❌ Missing accessibility
<Box as="nav"><MenuItems /></Box>
// ✅ Proper accessibility
<Box as="nav" aria-label="Main navigation"><MenuItems /></Box>Performance:
// ❌ Creates new component on every render
function Parent() {
const CustomDiv = (props) => <div {...props} />;
return <Component as={CustomDiv} />;
}
// ✅ Stable component reference
const CustomDiv = (props) => <div {...props} />;
function Parent() {
return <Component as={CustomDiv} />;
}Key Benefits
1. Semantic HTML Flexibility - Use the most appropriate element 2. Component Reusability - One component serves multiple purposes 3. Accessibility - Choose elements for best accessibility 4. Style System Integration - Maintain styling while changing elements
Core Component Principles
These six principles guide all component design decisions. Apply them consistently when building, refactoring, or evaluating components.
1. Composability and Reusability
Favor composition over inheritance. Build components that combine and nest to create complex UIs.
Incorrect (monolithic, hard to customize):
<Accordion data={data} />Correct (composable, each layer customizable):
<Accordion.Root open={open} onOpenChange={setOpen}>
{items.map((item) => (
<Accordion.Item key={item.id}>
<Accordion.Trigger>{item.title}</Accordion.Trigger>
<Accordion.Content>{item.content}</Accordion.Content>
</Accordion.Item>
))}
</Accordion.Root>2. Accessible by Default
Components must be usable by all users. Accessibility is not optional—it's a baseline feature.
Incorrect (generic div with click handler):
<div onClick={handleClick} className="button">Click me</div>Correct (semantic button element):
<button onClick={handleClick}>Click me</button>Best practices:
- Use semantic HTML (
<button>,<ul>/<li>,<nav>, etc.) - Provide proper ARIA roles, states, and properties
- Support keyboard navigation for all interactive elements
- Ensure focus management in modals, dropdowns, and overlays
3. Customizability and Theming
Components should be easy to restyle or adapt to different design requirements.
Correct (design tokens and className override):
export function Button({ className, ...props }: ButtonProps) {
return (
<button
className={cn('base-button-styles', className)}
{...props}
/>
);
}Best practices:
- Use CSS variables (design tokens) for colors, spacing, typography
- Always allow
classNameprop for style overrides - Provide sensible default styling
- Avoid hard-coded colors, fonts, or spacing values
4. Lightweight and Performant
Components should be lean in terms of assets and dependencies.
Incorrect (heavy dependency for simple task):
import { entireDateLibrary } from 'heavy-date-lib';Correct (native APIs or lightweight alternatives):
const formatDate = (date: Date) => date.toLocaleDateString();Best practices:
- Keep bundle size minimal
- Use tree-shaking friendly imports
- Lazy load heavy features when possible
- Optimize for initial render performance
5. Transparency and Code Ownership
Components should not be black boxes. Developers should be able to inspect and modify them.
Correct (clear, readable implementation):
export function Button({ children, onClick, ...props }: ButtonProps) {
return (
<button onClick={onClick} {...props}>
{children}
</button>
);
}Incorrect (obfuscated or overly complex):
export const Button = compose(withHOC1, withHOC2, withHOC3)(BaseButton);6. Well-documented and DX-Friendly
Components should come with clear documentation and examples.
Correct (comprehensive JSDoc):
/**
* Button component for primary actions.
*
* @example
* <Button variant="primary" onClick={handleClick}>Click me</Button>
*
* @remarks
* - Supports keyboard navigation (Enter/Space)
* - Accessible by default with proper ARIA attributes
*/
export function Button({ ... }: ButtonProps) { }A well-designed component applies all six principles together: Composes with other components, works for everyone with proper accessibility, adapts to different designs via theming, performs efficiently, can be inspected and modified, and is easy to learn through documentation.
Component Registries
Registries distribute source code, not compiled packages. This enables true component ownership and customization.
Core Concept
Bad:
// Traditional npm - compiled dependency
import { Button } from 'some-ui-library';Good:
// Registry-based - source code in your project
import { Button } from '@/components/ui/button';Registry Metadata Format
Bad:
{
"name": "announcement",
"type": "registry:component",
"description": "A compound badge component"
// Missing dependencies, registryDependencies, files, and category
}Good:
{
"name": "announcement",
"type": "registry:component",
"description": "A compound badge component",
"dependencies": ["class-variance-authority", "lucide-react"],
"registryDependencies": ["badge"],
"files": [
{
"type": "registry:component",
"path": "announcement.tsx",
"content": "..."
}
],
"category": "ui"
}Quick Publishing
1. Create structure:
my-component/
├── public/
│ └── metric-card.json
└── vercel.json2. Configure headers (`vercel.json`):
Example:
{
"headers": [
{
"source": "/(.*).json",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" },
{ "key": "Content-Type", "value": "application/json" }
]
}
]
}3. Deploy:
Example:
vercel --prod4. Users install via:
Example:
npx shadcn@latest add https://your-project.vercel.app/metric-card.jsonComponent JSON Structure
Example:
{
"name": "metric-card",
"type": "registry:component",
"description": "Display metrics with icon and trend",
"dependencies": ["lucide-react"],
"registryDependencies": ["card"],
"files": [
{
"type": "registry:component",
"path": "metric-card.tsx",
"content": "import { Card } from '@/components/ui/card'\n\nexport function MetricCard({ title, value }) {\n return <Card><h3>{title}</h3><p>{value}</p></Card>\n}"
}
],
"category": "ui"
}Registry vs npm
| Aspect | Registry | npm |
|---|---|---|
| Distribution | Source code | Compiled |
| Ownership | Full control | Dependency lock |
| Customization | Modify freely | Fork/override |
| Updates | Manual copy | npm update |
| Bundle size | Only what you use | Full package |
Best Practices
For Authors: 1. Document dependencies 2. Version components 3. Provide examples 4. Test compatibility 5. Use semantic naming
For Consumers: 1. Review source code 2. Check dependencies 3. Customize freely 4. Track updates 5. Test thoroughly
When to Use
Use registries when:
- Sharing with community
- Users need source ownership
- Building for specific frameworks
- Quick distribution needed
Use npm when:
- Distributing compiled code
- Need version management
- Framework-agnostic libraries
- Complex build processes
State Management Patterns
Build flexible components that work seamlessly in both controlled and uncontrolled modes.
Uncontrolled State
Component manages its own state internally:
export const Stepper = () => {
const [value, setValue] = useState(0);
return (
<div>
<p>{value}</p>
<button onClick={() => setValue(value + 1)}>Increment</button>
</div>
);
};When to use: Simple components, default behavior, internal state is sufficient.
Controlled State
Parent component manages the state:
type StepperProps = {
value: number;
setValue: (value: number) => void;
};
export const Stepper = ({ value, setValue }: StepperProps) => (
<div>
<p>{value}</p>
<button onClick={() => setValue(value + 1)}>Increment</button>
</div>
);When to use: State coordination, external data sources, form validation, persistence.
Merging Both Patterns
Use @radix-ui/react-use-controllable-state to support both:
import { useControllableState } from '@radix-ui/react-use-controllable-state';
type StepperProps = {
value?: number;
defaultValue?: number;
onValueChange?: (value: number) => void;
};
export const Stepper = ({ value: controlledValue, defaultValue, onValueChange }: StepperProps) => {
const [value, setValue] = useControllableState({
prop: controlledValue,
defaultProp: defaultValue,
onChange: onValueChange,
});
return (
<div>
<p>{value}</p>
<button onClick={() => setValue(value + 1)}>Increment</button>
</div>
);
};Usage:
// Uncontrolled
<Stepper defaultValue={0} />
// Controlled
const [count, setCount] = useState(5);
<Stepper value={count} onValueChange={setCount} />Prop Naming Conventions
| State Type | Value Prop | Default Prop | Change Callback |
|---|---|---|---|
| Generic | value | defaultValue | onValueChange |
| Boolean | checked | defaultChecked | onCheckedChange |
| Open/Close | open | defaultOpen | onOpenChange |
| Selection | selected | defaultSelected | onSelectedChange |
Complete Example: Toggle
import { useControllableState } from '@radix-ui/react-use-controllable-state';
type ToggleProps = {
checked?: boolean;
defaultChecked?: boolean;
onCheckedChange?: (checked: boolean) => void;
disabled?: boolean;
};
export const Toggle = ({ checked: controlledChecked, defaultChecked, onCheckedChange, disabled }: ToggleProps) => {
const [checked, setChecked] = useControllableState({
prop: controlledChecked,
defaultProp: defaultChecked ?? false,
onChange: onCheckedChange,
});
return (
<button
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => !disabled && setChecked(!checked)}
>
{checked ? 'On' : 'Off'}
</button>
);
};Best Practices
1. Always Make Props Optional:
// ✅ Supports both patterns
type Props = {
value?: number;
defaultValue?: number;
onValueChange?: (value: number) => void;
};2. Provide Sensible Defaults:
const [value, setValue] = useControllableState({
prop: controlledValue,
defaultProp: defaultValue ?? 0,
onChange: onValueChange,
});3. Handle onChange in Both Modes:
The onChange callback should fire regardless of controlled/uncontrolled mode.
Key Benefits
1. Flexibility - Works in both controlled and uncontrolled modes 2. Developer Experience - Simple API for simple cases, powerful for complex 3. Consistency - Matches patterns used by Radix UI and professional libraries 4. Backward Compatibility - Easy to migrate from uncontrolled to controlled
Component Styling with Tailwind CSS
Use Tailwind CSS with intelligent class merging (tailwind-merge), conditional classes (clsx), and variant APIs (CVA).
The cn Utility Function
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Why: Without tailwind-merge, conflicting classes both apply. The cn utility resolves conflicts intelligently.
Class Merging
Incorrect:
// Without tailwind-merge, conflicting classes both apply
className="bg-red-500 bg-blue-500" // Both classes apply, causing conflicts
className="px-4 py-2 px-8" // Both px-4 and px-8 applyCorrect:
twMerge('bg-red-500', 'bg-blue-500'); // "bg-blue-500"
twMerge('px-4 py-2', 'px-8'); // "py-2 px-8"
twMerge('text-sm', 'text-lg'); // "text-lg"Component Pattern: Order Matters
Apply classes in this order:
1. Base styles (always applied) 2. Variant styles (based on props) 3. Conditional styles (based on state) 4. User overrides (className prop)
Incorrect:
// Wrong order: user className comes before variants, preventing overrides
className={cn(className, variant === 'primary' && 'bg-blue-500')}Correct:
const Component = ({ className, variant, isActive, ...props }: ComponentProps) => {
return (
<div
className={cn(
'rounded-lg border bg-white shadow-sm', // 1. Base
variant === 'primary' && 'bg-blue-500', // 2. Variants
isActive && 'ring-2 ring-blue-500', // 3. Conditionals
className // 4. User overrides
)}
{...props}
/>
);
};Conditional Classes with clsx
cn('base', isActive && 'active');
cn('base', { 'active': isActive, 'disabled': isDisabled });
cn(['base', isLarge ? 'text-lg' : 'text-sm']);Class Variance Authority (CVA)
For components with multiple variants:
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-all",
{
variants: {
variant: {
default: "bg-blue-500 text-white hover:bg-blue-600",
destructive: "bg-red-500 text-white hover:bg-red-600",
outline: "border border-gray-300 bg-transparent hover:bg-gray-50",
ghost: "hover:bg-gray-100",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3 text-sm",
lg: "h-10 px-6",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
type ButtonProps = React.ComponentProps<'button'> & VariantProps<typeof buttonVariants>;
export const Button = ({ className, variant, size, ...props }: ButtonProps) => {
return (
<button className={cn(buttonVariants({ variant, size }), className)} {...props} />
);
};Key points:
- Define CVA variants outside the component
- Use
VariantProps<typeof variants>for TypeScript types - Always merge with
classNameprop usingcn
Best Practices
1. Extract Repeated Patterns:
export const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
export const disabled = 'disabled:pointer-events-none disabled:opacity-50';
className={cn(focusRing, disabled, className)}2. Use CSS Variables for Dynamic Values:
Incorrect:
// Dynamic class generation (not detected by Tailwind)
<div className={`bg-[${dynamicColor}]`} />Correct:
// CSS variables
<div className="bg-[var(--color)]" style={{ '--color': dynamicColor } as React.CSSProperties} />3. Document Variants:
type ButtonProps = {
/** The visual style @default "default" */
variant?: 'default' | 'destructive' | 'outline' | 'ghost';
/** The size @default "default" */
size?: 'sm' | 'default' | 'lg' | 'icon';
};Common Patterns
State-Based Styling:
<div className={cn(
'transition-all',
isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
)} />CVA with Additional Conditionals:
<div className={cn(
baseVariants({ variant, size }),
isActive && 'ring-2 ring-blue-500',
isDisabled && 'opacity-50 cursor-not-allowed',
className
)} />Component Types
Proper typing is essential for creating flexible, customizable, and type-safe component interfaces.
Single Element Wrapping
Each exported component should wrap a single HTML or JSX element:
Incorrect (hard to customize):
const Card = ({ title, description, footer, ...props }) => (
<div {...props}>
<div className="card-header">
<h2>{title}</h2>
<p>{description}</p>
</div>
<div className="card-footer">{footer}</div>
</div>
);Correct (composable, each layer customizable):
<Card.Root>
<Card.Header>
<Card.Title>Title</Card.Title>
<Card.Description>Description</Card.Description>
</Card.Header>
<Card.Footer>Footer</Card.Footer>
</Card.Root>Extending HTML Attributes
Every component should extend native HTML attributes:
export type CardRootProps = React.ComponentProps<'div'> & {
variant?: 'default' | 'outlined';
};
export const CardRoot = ({ variant = 'default', ...props }: CardRootProps) => (
<div {...props} />
);Common HTML Attribute Types:
type DivProps = React.ComponentProps<'div'>;
type ButtonProps = React.ComponentProps<'button'>;
type InputProps = React.ComponentProps<'input'>;
type FormProps = React.ComponentProps<'form'>;
type LinkProps = React.ComponentProps<'a'>;Exporting Types
Always export prop types for consumers:
// Enables type extraction
import type { CardRootProps } from '@/components/ui/card';
type Variant = CardRootProps['variant'];
// Enables extending
export type ExtendedCardProps = CardRootProps & { isLoading?: boolean };
// Enables wrapper components
const MyCard = (props: CardRootProps) => (
<CardRoot {...props} className={cn('my-custom-class', props.className)} />
);Naming convention: Export types as <ComponentName>Props.
Best Practices
1. Always Spread Props Last:
// ✅ User props override defaults
<div className="default-class" {...props} />
// ❌ Defaults override user props
<div {...props} className="default-class" />2. Avoid Prop Name Conflicts:
// ❌ Conflicts with HTML title attribute
type CardProps = React.ComponentProps<'div'> & { title: string };
// ✅ Use a different name
type CardProps = React.ComponentProps<'div'> & { heading: string };3. Document Custom Props:
export type DialogProps = React.ComponentProps<'div'> & {
/** Whether the dialog is currently open */
open: boolean;
/** Callback when the dialog requests to be closed */
onOpenChange: (open: boolean) => void;
/** Whether to render the dialog in a portal */
modal?: boolean;
};Polymorphic Types
For components that can render as different elements:
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
} & React.ComponentPropsWithoutRef<E>;
function Component<E extends React.ElementType = 'div'>({
as,
...props
}: PolymorphicProps<E>) {
const Element = as || 'div';
return <Element {...props} />;
}Quick Reference
| Pattern | Usage | Example |
|---|---|---|
| Basic extension | Extend single HTML element | React.ComponentProps<'div'> |
| Custom props | Add component-specific props | React.ComponentProps<'button'> & { variant?: string } |
| Polymorphic | Render as different elements | PolymorphicProps<T> |
| Type extraction | Get specific prop type | CardProps['variant'] |
| Type extension | Extend existing component props | CardProps & { isLoading?: boolean } |