---
title: Accessibility
description: Building components that are usable by everyone, including users with disabilities who rely on assistive technologies.
type: guide
summary: Semantic HTML, keyboard navigation, ARIA patterns, focus management, color contrast, and common accessibility pitfalls.
prerequisites:
  - /definitions
  - /composition
related:
  - /data-attributes
  - /types
---

# Accessibility



Accessibility (a11y) is not an optional feature—it's a fundamental requirement for modern web components. Every component must be usable by everyone, including people with visual, motor, auditory, or cognitive disabilities.

This guide is a non-exhaustive list of accessibility principles and patterns that you should follow when building components. It's not a comprehensive guide, but it should give you a sense of the types of issues you should be aware of.

If you use a linter with strong accessibility rules like [Ultracite](https://www.ultracite.ai), these types of issues will likely be caught automatically, but it's still important to understand the principles.

## Core Principles

### 1. Semantic HTML First

Always start with the most appropriate HTML element. Semantic HTML provides built-in accessibility features that custom implementations often miss.

```tsx
// ❌ Don't reinvent the wheel
<div onClick={handleClick} className="button">
  Click me
</div>

// ✅ Use semantic elements
<button onClick={handleClick}>
  Click me
</button>
```

Semantic elements come with proper role announcements, keyboard interaction, focus management, and form participation.

### 2. Keyboard Navigation

Every interactive element must be keyboard accessible. Users should be able to navigate, activate, and interact with all functionality using only a keyboard.

```tsx
// ✅ Complete keyboard support
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}>
      {/* menu items */}
    </div>
  );
}
```

### 3. Screen Reader Support

Ensure all content and interactions are announced properly to screen readers using ARIA attributes when necessary.

```tsx
// ✅ Proper ARIA labeling
<nav aria-label="Main navigation">
  <ul>
    <li><a href="/" aria-current="page">Home</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

// ✅ Dynamic content announcements
<div aria-live="polite" aria-atomic="true">
  {isLoading && <span>Loading results...</span>}
  {results && <span>{results.length} results found</span>}
</div>
```

### 4. Visual Accessibility

Support users with visual impairments through proper contrast, focus indicators, and responsive text sizing.

```css
/* ✅ Visible focus indicators */
button:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* ✅ Sufficient color contrast (4.5:1 for normal text, 3:1 for large text) */
.text {
  color: #333; /* Against white: 12.6:1 ratio */
  background: white;
}

/* ✅ Responsive text sizing */
.text {
  font-size: 1rem; /* Respects user preferences */
}
```

## ARIA Patterns

### Understanding ARIA

ARIA (Accessible Rich Internet Applications) provides semantic information about elements to assistive technologies. Use ARIA to enhance, not replace, semantic HTML.

It has a few rules that you should follow:

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 from assistive technologies
5. All interactive elements must have accessible names

### Common ARIA Attributes

#### Roles

Define what an element is:

```tsx
// Widget roles
<div role="button" tabIndex={0} onClick={handleClick}>
  Custom Button
</div>

// Landmark roles
<div role="navigation" aria-label="Breadcrumb">
  {/* breadcrumb items */}
</div>

// Live region roles
<div role="alert">
  Error: Invalid email address
</div>
```

#### States

Describe the current state of an element:

```tsx
// Checked state
<div
  role="checkbox"
  aria-checked={isChecked}
  tabIndex={0}
>
  Accept terms
</div>

// Expanded state
<button
  aria-expanded={isOpen}
  aria-controls="panel-1"
>
  Toggle Panel
</button>
<div id="panel-1" hidden={!isOpen}>
  Panel content
</div>

// Selected state
<li
  role="option"
  aria-selected={isSelected}
>
  Option 1
</li>
```

#### Properties

Provide additional information:

```tsx
// Labels and descriptions
<input
  aria-label="Search"
  aria-describedby="search-help"
  type="search"
/>
<span id="search-help">Press Enter to search</span>

// Relationships
<button aria-controls="modal-1">Open Modal</button>
<div id="modal-1" role="dialog">{/* modal content */}</div>

// Required and invalid
<input
  aria-required="true"
  aria-invalid={hasError}
  aria-errormessage="email-error"
/>
<span id="email-error">Please enter a valid email</span>
```

## Component Patterns

### Modal/Dialog

Modals require careful focus management and keyboard trapping:

```tsx
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocus = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      // Store current focus
      previousFocus.current = document.activeElement as HTMLElement;

      // Focus first focusable element in modal
      const firstFocusable = modalRef.current?.querySelector<HTMLElement>(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
      );
      firstFocusable?.focus();

      // Prevent body scroll
      document.body.style.overflow = "hidden";
    } else {
      // Restore focus
      previousFocus.current?.focus();
      document.body.style.overflow = "";
    }

    return () => {
      document.body.style.overflow = "";
    };
  }, [isOpen]);

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === "Escape") {
      onClose();
    }

    if (e.key === "Tab") {
      // Trap focus within modal
      const focusables = modalRef.current?.querySelectorAll<HTMLElement>(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
      );

      if (focusables && focusables.length > 0) {
        const firstFocusable = focusables[0];
        const lastFocusable = focusables[focusables.length - 1];

        if (e.shiftKey && document.activeElement === firstFocusable) {
          e.preventDefault();
          lastFocusable.focus();
        } else if (!e.shiftKey && document.activeElement === lastFocusable) {
          e.preventDefault();
          firstFocusable.focus();
        }
      }
    }
  };

  if (!isOpen) return null;

  return (
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      ref={modalRef}
      onKeyDown={handleKeyDown}
      className="modal"
    >
      <button
        onClick={onClose}
        aria-label="Close dialog"
        className="close-button"
      >
        ×
      </button>
      {children}
    </div>
  );
}
```

### Dropdown Menu

Dropdowns need proper ARIA attributes and keyboard navigation:

```tsx
function DropdownMenu({ items }) {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(-1);
  const menuRef = useRef<HTMLUListElement>(null);

  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        if (!isOpen) {
          setIsOpen(true);
          setSelectedIndex(0);
        } else {
          setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
        }
        break;

      case "ArrowUp":
        e.preventDefault();
        if (isOpen) {
          setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
        }
        break;

      case "Enter":
      case " ":
        e.preventDefault();
        if (!isOpen) {
          setIsOpen(true);
        } else if (selectedIndex >= 0) {
          items[selectedIndex].onClick();
          setIsOpen(false);
        }
        break;

      case "Escape":
        setIsOpen(false);
        setSelectedIndex(-1);
        break;
    }
  };

  return (
    <div className="dropdown">
      <button
        aria-haspopup="true"
        aria-expanded={isOpen}
        aria-controls="dropdown-menu"
        onKeyDown={handleKeyDown}
        onClick={() => setIsOpen(!isOpen)}
      >
        Menu
      </button>

      {isOpen && (
        <ul
          id="dropdown-menu"
          role="menu"
          ref={menuRef}
          onKeyDown={handleKeyDown}
        >
          {items.map((item, index) => (
            <li
              key={item.id}
              role="menuitem"
              tabIndex={-1}
              aria-selected={index === selectedIndex}
              onClick={() => {
                item.onClick();
                setIsOpen(false);
              }}
            >
              {item.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

### Tabs

Tab interfaces require specific ARIA patterns and keyboard navigation:

```tsx
function Tabs({ tabs, defaultTab = 0 }) {
  const [activeTab, setActiveTab] = useState(defaultTab);

  const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
    let newIndex = index;

    switch (e.key) {
      case "ArrowLeft":
        newIndex = index > 0 ? index - 1 : tabs.length - 1;
        break;
      case "ArrowRight":
        newIndex = index < tabs.length - 1 ? index + 1 : 0;
        break;
      case "Home":
        newIndex = 0;
        break;
      case "End":
        newIndex = tabs.length - 1;
        break;
      default:
        return;
    }

    e.preventDefault();
    setActiveTab(newIndex);

    // Focus the newly selected tab
    const tabElement = document.getElementById(`tab-${newIndex}`);
    tabElement?.focus();
  };

  return (
    <div className="tabs">
      <div role="tablist" aria-label="Tabs">
        {tabs.map((tab, index) => (
          <button
            key={tab.id}
            id={`tab-${index}`}
            role="tab"
            aria-selected={activeTab === index}
            aria-controls={`panel-${index}`}
            tabIndex={activeTab === index ? 0 : -1}
            onClick={() => setActiveTab(index)}
            onKeyDown={(e) => handleKeyDown(e, index)}
          >
            {tab.label}
          </button>
        ))}
      </div>

      {tabs.map((tab, index) => (
        <div
          key={tab.id}
          id={`panel-${index}`}
          role="tabpanel"
          aria-labelledby={`tab-${index}`}
          hidden={activeTab !== index}
          tabIndex={0}
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}
```

### Forms

Forms need clear labels, error messages, and validation feedback:

```tsx
function AccessibleForm() {
  const [errors, setErrors] = useState({});

  return (
    <form aria-label="Contact form">
      <div className="form-group">
        <label htmlFor="email">
          Email Address
          <span aria-label="required">*</span>
        </label>
        <input
          id="email"
          type="email"
          aria-required="true"
          aria-invalid={!!errors.email}
          aria-describedby={errors.email ? "email-error" : "email-help"}
        />
        <span id="email-help" className="help-text">
          We'll never share your email
        </span>
        {errors.email && (
          <span id="email-error" role="alert" className="error">
            {errors.email}
          </span>
        )}
      </div>

      <fieldset>
        <legend>Notification Preferences</legend>
        <div>
          <input
            id="notify-email"
            type="checkbox"
            name="notifications"
            value="email"
          />
          <label htmlFor="notify-email">Email notifications</label>
        </div>
        <div>
          <input
            id="notify-sms"
            type="checkbox"
            name="notifications"
            value="sms"
          />
          <label htmlFor="notify-sms">SMS notifications</label>
        </div>
      </fieldset>

      <button type="submit">Submit</button>
    </form>
  );
}
```

## Focus Management

### Focus Visible

Show focus indicators only for keyboard navigation:

```css
/* Remove default outline */
*:focus {
  outline: none;
}

/* Show outline only for keyboard focus */
*:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Custom focus styles for specific components */
.button:focus-visible {
  box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
}
```

### Focus Trapping

Keep focus within a specific region:

```tsx
function useFocusTrap(ref: React.RefObject<HTMLElement>, isActive: boolean) {
  useEffect(() => {
    if (!isActive || !ref.current) return;

    const element = ref.current;
    const focusableSelector =
      'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, [tabindex]:not([tabindex="-1"])';

    const focusableElements =
      element.querySelectorAll<HTMLElement>(focusableSelector);
    const firstFocusable = focusableElements[0];
    const lastFocusable = focusableElements[focusableElements.length - 1];

    const handleTabKey = (e: KeyboardEvent) => {
      if (e.key !== "Tab") return;

      if (e.shiftKey) {
        if (document.activeElement === firstFocusable) {
          e.preventDefault();
          lastFocusable?.focus();
        }
      } else {
        if (document.activeElement === lastFocusable) {
          e.preventDefault();
          firstFocusable?.focus();
        }
      }
    };

    element.addEventListener("keydown", handleTabKey);
    firstFocusable?.focus();

    return () => {
      element.removeEventListener("keydown", handleTabKey);
    };
  }, [ref, isActive]);
}
```

### Focus Restoration

Return focus to the appropriate element after interactions:

```tsx
function useRestoreFocus() {
  const previousFocus = useRef<HTMLElement | null>(null);

  const saveFocus = () => {
    previousFocus.current = document.activeElement as HTMLElement;
  };

  const restoreFocus = () => {
    previousFocus.current?.focus();
  };

  return { saveFocus, restoreFocus };
}
```

## Live Regions

Announce dynamic content changes to screen readers:

### Status Messages

```tsx
// Polite announcement (waits for screen reader to finish)
<div role="status" aria-live="polite">
  {savedMessage && "Settings saved successfully"}
</div>

// Assertive announcement (interrupts screen reader)
<div role="alert" aria-live="assertive">
  {errorMessage && `Error: ${errorMessage}`}
</div>

// Loading states
<div aria-live="polite" aria-busy={isLoading}>
  {isLoading ? "Loading..." : `${items.length} items loaded`}
</div>
```

### Progress Indicators

```tsx
function ProgressBar({ value, max = 100 }) {
  return (
    <div
      role="progressbar"
      aria-valuenow={value}
      aria-valuemin={0}
      aria-valuemax={max}
      aria-label="Upload progress"
    >
      <div
        className="progress-fill"
        style={{ width: `${(value / max) * 100}%` }}
      />
      <span className="sr-only">
        {Math.round((value / max) * 100)}% complete
      </span>
    </div>
  );
}
```

## Color and Contrast

### Contrast Requirements

Follow WCAG guidelines for color contrast:

```css
/* Normal text (< 18pt or < 14pt bold) */
.text {
  color: #595959; /* 7:1 ratio against white */
  background: white;
}

/* Large text (≥ 18pt or ≥ 14pt bold) */
.heading {
  color: #767676; /* 4.5:1 ratio against white */
  font-size: 1.5rem;
  font-weight: bold;
}

/* Non-text elements (icons, borders) */
.icon {
  color: #949494; /* 3:1 ratio against white */
}
```

### Color Independence

Never convey information through color alone:

```tsx
// ❌ Color only
<span className="text-red-500">Error</span>

// ✅ Color with text/icon
<span className="text-red-500">
  <ErrorIcon aria-hidden="true" />
  <span>Error: Invalid input</span>
</span>

// ✅ Multiple indicators
<input
  className={hasError ? 'border-red-500' : 'border-gray-300'}
  aria-invalid={hasError}
  aria-describedby={hasError ? 'error-message' : undefined}
/>
{hasError && (
  <span id="error-message" className="text-red-500">
    <ErrorIcon aria-hidden="true" /> This field is required
  </span>
)}
```

## Mobile Accessibility

### Touch Targets

Ensure touch targets are large enough:

```css
/* Minimum 44x44px for iOS, 48x48dp for Android */
.button {
  min-height: 44px;
  min-width: 44px;
  padding: 12px 16px;
}

/* Add invisible touch area for small icons */
.icon-button {
  position: relative;
  padding: 8px;
}

.icon-button::before {
  content: "";
  position: absolute;
  top: -8px;
  right: -8px;
  bottom: -8px;
  left: -8px;
}
```

### Viewport and Zoom

Allow users to zoom:

```html
<!-- ✅ Allows zooming -->
<meta name="viewport" content="width=device-width, initial-scale=1" />

<!-- ❌ Prevents zooming -->
<meta
  name="viewport"
  content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
```

## Common Pitfalls

### Placeholder Text as Labels

Don't use placeholders as the only label:

```tsx
// ❌ Placeholder disappears when typing
<input placeholder="Email address" />

// ✅ Persistent label
<label>
  Email address
  <input type="email" />
</label>

// ✅ Floating label pattern
<div className="form-field">
  <input id="email" placeholder=" " />
  <label htmlFor="email">Email address</label>
</div>
```

### Empty Buttons

Always provide accessible text for icon buttons:

```tsx
// ❌ No accessible name
<button onClick={handleDelete}>
  <TrashIcon />
</button>

// ✅ Screen reader text
<button onClick={handleDelete} aria-label="Delete item">
  <TrashIcon aria-hidden="true" />
</button>

// ✅ Visually hidden text
<button onClick={handleDelete}>
  <TrashIcon aria-hidden="true" />
  <span className="sr-only">Delete item</span>
</button>
```

### Disabled Form Elements

Disabled elements aren't focusable, which can confuse users:

```tsx
// ❌ User can't understand why button is disabled
<button disabled={!isValid}>
  Submit
</button>

// ✅ Use aria-disabled and explain
<button
  aria-disabled={!isValid}
  aria-describedby="submit-help"
  onClick={isValid ? handleSubmit : undefined}
  className={!isValid ? 'opacity-50 cursor-not-allowed' : ''}
>
  Submit
</button>
<span id="submit-help">
  {!isValid && 'Please fill in all required fields'}
</span>
```


---
title: asChild
description: How to use the `asChild` prop to render a custom element within the component.
type: reference
summary: Using the asChild prop to replace default markup with custom elements while preserving component functionality and event handlers.
prerequisites:
  - /composition
  - /types
related:
  - /polymorphism
  - /state
---

# asChild



The `asChild` prop is a powerful pattern in modern React component libraries. Popularized by [Radix UI](https://www.radix-ui.com/primitives/docs/guides/composition) and adopted by [shadcn/ui](https://ui.shadcn.com), this pattern allows you to replace default markup with custom elements while maintaining the component's functionality.

## Understanding `asChild`

At its core, `asChild` changes how a component renders. When set to `true`, instead of rendering its default DOM element, the component merges its props, behaviors, and event handlers with its immediate child element.

### Without `asChild`

```tsx
<Dialog.Trigger>
  <button>Open Dialog</button>
</Dialog.Trigger>
```

This renders nested elements:

```html
<button data-state="closed">
  <button>Open Dialog</button>
</button>
```

### With `asChild`

```tsx
<Dialog.Trigger asChild>
  <button>Open Dialog</button>
</Dialog.Trigger>
```

This renders a single, merged element:

```html
<button data-state="closed">Open Dialog</button>
```

The Dialog.Trigger's functionality is composed onto your button, eliminating unnecessary wrapper elements.

## How It Works

Under the hood, `asChild` uses React's composition capabilities to merge components:

```tsx
// Simplified implementation
function Component({ asChild, children, ...props }) {
  if (asChild) {
    // Clone child and merge props
    return React.cloneElement(children, {
      ...props,
      ...children.props,
      // Merge event handlers
      onClick: (e) => {
        props.onClick?.(e);
        children.props.onClick?.(e);
      },
    });
  }

  // Render default element
  return <button {...props}>{children}</button>;
}
```

The component:

1. Checks if `asChild` is true
2. Clones the child element
3. Merges props from both parent and child
4. Combines event handlers
5. Returns the enhanced child

## Key Benefits

### 1. Semantic HTML

`asChild` lets you use the most appropriate HTML element for your use case:

```tsx
// Use a link for navigation
<AlertDialog.Trigger asChild>
  <a href="/delete">Delete Account</a>
</AlertDialog.Trigger>

// Use a custom button component
<Tooltip.Trigger asChild>
  <IconButton icon={<InfoIcon />} />
</Tooltip.Trigger>
```

### 2. Clean DOM Structure

Traditional composition often creates deeply nested DOM structures. `asChild` eliminates this "wrapper hell":

```tsx
// Without asChild: Nested wrappers
<TooltipProvider>
  <Tooltip>
    <TooltipTrigger>
      <button>
        <span>Hover me</span>
      </button>
    </TooltipTrigger>
  </Tooltip>
</TooltipProvider>

// With asChild: Clean structure
<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild>
      <button>Hover me</button>
    </TooltipTrigger>
  </Tooltip>
</TooltipProvider>
```

### 3. Design System Integration

`asChild` enables seamless integration with your existing design system components:

```tsx
import { Button } from "@/components/ui/button";

<DropdownMenu.Trigger asChild>
  <Button variant="outline" size="icon">
    <MoreVertical className="h-4 w-4" />
  </Button>
</DropdownMenu.Trigger>;
```

Your Button component receives all the necessary dropdown trigger behavior without modification.

### 4. Component Composition

You can compose multiple behaviors onto a single element:

```tsx
<Dialog.Trigger asChild>
  <Tooltip.Trigger asChild>
    <button>Open dialog (with tooltip)</button>
  </Tooltip.Trigger>
</Dialog.Trigger>
```

This creates a button that both opens a dialog and shows a tooltip on hover.

## Common Use Cases

### Custom Trigger Elements

Replace default triggers with custom components:

```tsx
// Custom link trigger
<Collapsible.Trigger asChild>
  <a href="#" className="text-blue-600 underline">
    Toggle Details
  </a>
</Collapsible.Trigger>

// Icon-only trigger
<Popover.Trigger asChild>
  <IconButton>
    <Settings className="h-4 w-4" />
  </IconButton>
</Popover.Trigger>
```

### Accessible Navigation

Maintain proper semantics for navigation elements:

```tsx
<NavigationMenu.Link asChild>
  <Link href="/products" className="nav-link">
    Products
  </Link>
</NavigationMenu.Link>
```

### Form Integration

Integrate with form libraries while preserving functionality:

```tsx
<FormField
  control={form.control}
  name="acceptTerms"
  render={({ field }) => (
    <FormItem>
      <Checkbox.Root asChild>
        <input type="checkbox" {...field} className="sr-only" />
      </Checkbox.Root>
    </FormItem>
  )}
/>
```

## Best Practices

### 1. Maintain Accessibility

When changing element types, ensure accessibility is preserved:

```tsx
// ✅ Good - maintains button semantics
<Dialog.Trigger asChild>
  <button type="button">Open</button>
</Dialog.Trigger>

// ⚠️ Caution - ensure proper ARIA attributes
<Dialog.Trigger asChild>
  <div role="button" tabIndex={0}>Open</div>
</Dialog.Trigger>
```

### 2. Document Component Requirements

Clearly document when components support `asChild`:

```tsx
interface TriggerProps {
  /**
   * Change the default rendered element for the one passed as a child,
   * merging their props and behavior.
   *
   * @default false
   */
  asChild?: boolean;
  children: React.ReactNode;
}
```

### 3. Test Child Components

Verify that custom components work correctly with `asChild`:

```tsx
// Test that props are properly forwarded
const TestButton = (props) => {
  console.log("Received props:", props);
  return <button {...props} />;
};

<Tooltip.Trigger asChild>
  <TestButton>Test</TestButton>
</Tooltip.Trigger>;
```

### 4. Handle Edge Cases

Consider edge cases like conditional rendering:

```tsx
// Handle conditional children
<Dialog.Trigger asChild>
  {isLoading ? (
    <Skeleton className="h-10 w-20" />
  ) : (
    <Button>Open Dialog</Button>
  )}
</Dialog.Trigger>
```

## Common Pitfalls

### Not Spreading Props

As discussed in [Types](/docs/types), you should always spread props to the underlying element.

```tsx
// ❌ Won't receive trigger behavior
const BadButton = ({ children }) => <button>{children}</button>;

// ✅ Properly receives all props
const GoodButton = ({ children, ...props }) => (
  <button {...props}>{children}</button>
);
```

### Multiple Children

Don't pass multiple children to a component that supports `asChild`. This will cause an error as the component will not know which child to use.

```tsx
// ❌ Error - asChild expects single child
<Trigger asChild>
  <button>One</button>
  <button>Two</button>
</Trigger>

// ✅ Single child element
<Trigger asChild>
  <button>Single Button</button>
</Trigger>
```

### Fragment Children

Don't pass a fragment to a component that supports `asChild`. This will cause an error as fragments are not valid elements.

```tsx
// ❌ Fragment is not a valid element
<Trigger asChild>
  <>Button</>
</Trigger>

// ✅ Actual element
<Trigger asChild>
  <button>Button</button>
</Trigger>
```


---
title: Composition
description: The foundation of building modern UI components.
type: conceptual
summary: How to break monolithic components into smaller, focused subcomponents using the compound component pattern and naming conventions.
prerequisites:
  - /definitions
related:
  - /types
  - /state
  - /as-child
  - /polymorphism
---

# Composition



Composition, or composability, is the foundation of building modern UI components. It is one of the most powerful techniques for creating flexible, reusable components that can handle complex requirements without sacrificing API clarity.

Instead of cramming all functionality into a single component with dozens of props, composition distributes responsibility across multiple cooperating components.

Fernando gave a great talk about this at React Universe Conf 2025, where he shared his approach to rebuilding Slack's Message Composer as a composable component.

<Video src="https://www.youtube.com/watch?v=4KvbVq3Eg5w" />

## Making a component composable

To make a component composable, you need to break it down into smaller, more focused components. For example, let's take this Accordion component:

```tsx title="accordion.tsx"
import { Accordion } from "@/components/ui/accordion";

const data = [
  {
    title: "Accordion 1",
    content: "Accordion 1 content",
  },
  {
    title: "Accordion 2",
    content: "Accordion 2 content",
  },
  {
    title: "Accordion 3",
    content: "Accordion 3 content",
  },
];

return <Accordion data={data} />;
```

While this Accordion component might seem simple, it's handling too many responsibilities. It's responsible for rendering the container, trigger and content; as well as handling the accordion state and data.

Customizing the styling of this component is difficult because it's tightly coupled. It likely requires global CSS overrides. Additionally, adding new functionality or tweaking the behavior requires modifying the component source code.

To solve this, we can break this down into smaller, more focused components.

### 1. Root Component

First, let's focus on the container - the component that holds everything together i.e. the trigger and content. This container doesn't need to know about the data, but it does need to keep track of the open state.

However, we also want this state to be accessible by child components. So, let's use the Context API to create a context for the open state.

Finally, to allow for modification of the `div` element, we'll extend the default HTML attributes.

We'll call this component the "Root" component.

```tsx title="@/components/ui/accordion.tsx"
type AccordionProps = React.ComponentProps<"div"> & {
  open: boolean;
  setOpen: (open: boolean) => void;
};

const AccordionContext = createContext<AccordionProps>({
  open: false,
  setOpen: () => {},
});

export type AccordionRootProps = React.ComponentProps<"div"> & {
  open: boolean;
  setOpen: (open: boolean) => void;
};

export const Root = ({
  children,
  open,
  setOpen,
  ...props
}: AccordionRootProps) => (
  <AccordionContext.Provider value={{ open, setOpen }}>
    <div {...props}>{children}</div>
  </AccordionContext.Provider>
);
```

### 2. Item Component

The Item component is the element that contains the accordion item. It is simply a wrapper for each item in the accordion.

```tsx title="@/components/ui/accordion.tsx"
export type AccordionItemProps = React.ComponentProps<"div">;

export const Item = (props: AccordionItemProps) => <div {...props} />;
```

### 3. Trigger Component

The Trigger component is the element that opens the accordion when activated. It is responsible for:

* Rendering as a button by default (can be customized with `asChild`)
* Handling click events to open the accordion
* Managing focus when accordion closes
* Providing proper ARIA attributes

Let's add this component to our Accordion component.

```tsx title="@/components/ui/accordion.tsx"
export type AccordionTriggerProps = React.ComponentProps<"button"> & {
  asChild?: boolean;
};

export const Trigger = ({ asChild, ...props }: AccordionTriggerProps) => (
  <AccordionContext.Consumer>
    {({ open, setOpen }) => (
      <button onClick={() => setOpen(!open)} {...props} />
    )}
  </AccordionContext.Consumer>
);
```

### 4. Content Component

The Content component is the element that contains the accordion content. It is responsible for:

* Rendering the content when the accordion is open
* Providing proper ARIA attributes

Let's add this component to our Accordion component.

```tsx title="@/components/ui/accordion.tsx"
export type AccordionContentProps = React.ComponentProps<"div"> & {
  asChild?: boolean;
};

export const Content = ({ asChild, ...props }: AccordionContentProps) => (
  <AccordionContext.Consumer>
    {({ open }) => <div {...props} />}
  </AccordionContext.Consumer>
);
```

### 5. Putting it all together

Now that we have all the components, we can put them together in our original file.

```tsx title="accordion.tsx"
import * as Accordion from "@/components/ui/accordion";

const data = [
  {
    title: "Accordion 1",
    content: "Accordion 1 content",
  },
  {
    title: "Accordion 2",
    content: "Accordion 2 content",
  },
  {
    title: "Accordion 3",
    content: "Accordion 3 content",
  },
];

return (
  <Accordion.Root open={false} 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>
);
```

## Naming Conventions

When building composable components, consistent naming conventions are crucial for creating intuitive and predictable APIs. Both shadcn/ui and Radix UI follow established patterns that have become the de facto standard in the React ecosystem.

### Root Components

The `Root` component serves as the main container that wraps all other sub-components. It typically manages shared state and context by providing a context to all child components.

```tsx
<AccordionRoot>{/* Child components */}</AccordionRoot>
```

### Interactive Elements

Interactive components that trigger actions or toggle states use descriptive names:

* `Trigger` - The element that initiates an action (opening, closing, toggling)
* `Content` - The element that contains the main content being shown/hidden

```tsx
<CollapsibleTrigger>Click to expand</CollapsibleTrigger>
<CollapsibleContent>
  Hidden content revealed here
</CollapsibleContent>
```

### Content Structure

For components with structured content areas, use semantic names that describe their purpose:

* `Header` - Top section containing titles or controls
* `Body` - Main content area
* `Footer` - Bottom section for actions or metadata

```tsx
<DialogHeader>
  {/* Dialog title */}
</DialogHeader>
<DialogBody>
  {/* Dialog content */}
</DialogBody>
<DialogFooter>
  {/* Dialog footer */}
</DialogFooter>
```

### Informational Components

Components that provide information or context use descriptive suffixes:

* `Title` - Primary heading or label
* `Description` - Supporting text or explanatory content

```tsx
<CardTitle>Project Statistics</CardTitle>
<CardDescription>
  View your project's performance over time
</CardDescription>
```


---
title: Data Attributes
description: Using data attributes for declarative styling and component identification.
type: reference
summary: Using data-state for declarative visual state styling and data-slot for stable component identification in Tailwind-based systems.
prerequisites:
  - /composition
  - /styling
related:
  - /design-tokens
  - /accessibility
---

# Data Attributes



Data attributes provide a powerful way to expose component state and structure to consumers, enabling flexible styling without prop explosion. Modern component libraries use two primary patterns: `data-state` for visual states and `data-slot` for component identification.

## Styling state with data-state

One of the most common anti-patterns in component styling is exposing separate className props for different states.

In less modern components, you'll often see APIs like this:

```tsx
<Dialog
  openClassName="bg-black"
  closedClassName="bg-white"
  classes={{
    open: "opacity-100",
    closed: "opacity-0",
  }}
/>
```

This approach has several problems:

* It couples the component's internal state to its styling API
* It creates an explosion of props as components grow more complex
* It makes the component harder to use and maintain
* It prevents styling based on state combinations

### The solution: data-state attributes

Instead, use `data-*` attributes to expose component state declaratively. This allows consumers to style components based on state using standard CSS selectors:

```tsx title="component.tsx"
const Dialog = ({ className, ...props }: DialogProps) => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div
      data-state={isOpen ? "open" : "closed"}
      className={cn("transition-all", className)}
      {...props}
    />
  );
};
```

Now consumers can style the component based on state from the outside:

```tsx title="app.tsx"
<Dialog className="data-[state=open]:opacity-100 data-[state=closed]:opacity-0" />
```

### Benefits of this approach

1. **Single className prop** - No need for multiple state-specific className props
2. **Composable** - Combine multiple data attributes for complex states
3. **Standard CSS** - Works with any CSS-in-JS solution or plain CSS
4. **Type-safe** - TypeScript can infer data attribute values
5. **Inspectable** - States are visible in DevTools as HTML attributes

### Common state patterns

Use data attributes for all kinds of component state:

```tsx
// Open/closed state
<Accordion data-state={isOpen ? 'open' : 'closed'} />

// Selected state
<Tab data-state={isSelected ? 'active' : 'inactive'} />

// Disabled state (in addition to disabled attribute)
<Button data-disabled={isDisabled} disabled={isDisabled} />

// Loading state
<Button data-loading={isLoading} />

// Orientation
<Slider data-orientation="horizontal" />

// Side/position
<Tooltip data-side="top" />
```

### Styling with Tailwind

Tailwind supports arbitrary variants, making data attribute styling elegant:

```tsx
<Dialog
  className={cn(
    // Base styles
    "rounded-lg border p-4",
    // State-based styles
    "data-[state=open]:animate-in data-[state=open]:fade-in",
    "data-[state=closed]:animate-out data-[state=closed]:fade-out",
    // Multiple attributes
    "data-[state=open][data-side=top]:slide-in-from-top-2",
  )}
/>
```

For commonly-used states, you can extend Tailwind's configuration:

```js title="tailwind.config.js"
module.exports = {
  theme: {
    extend: {
      data: {
        open: 'state="open"',
        closed: 'state="closed"',
        active: 'state="active"',
      },
    },
  },
};
```

Now you can use shorthand:

```tsx
<Dialog className="data-open:opacity-100 data-closed:opacity-0" />
```

### Integration with Radix UI

This pattern is used extensively by [Radix UI](https://www.radix-ui.com/), which automatically applies data attributes to its primitives:

```tsx
import * as Dialog from "@radix-ui/react-dialog";

<Dialog.Root>
  <Dialog.Trigger />
  <Dialog.Portal>
    {/* Radix automatically adds data-state="open" | "closed" */}
    <Dialog.Overlay className="data-[state=open]:animate-in data-[state=closed]:animate-out" />
    <Dialog.Content className="data-[state=open]:fade-in data-[state=closed]:fade-out" />
  </Dialog.Portal>
</Dialog.Root>;
```

Other data attributes Radix provides include:

* `data-state` - open/closed, active/inactive, on/off
* `data-side` - top/right/bottom/left (for positioned elements)
* `data-align` - start/center/end (for positioned elements)
* `data-orientation` - horizontal/vertical
* `data-disabled` - present when disabled
* `data-placeholder` - present when showing placeholder

## Component identification with data-slot

While `data-state` tracks visual states, `data-slot` identifies component types within a composition. This pattern, popularized by [shadcn/ui](https://ui.shadcn.com/), allows parent components to target and style specific child components without relying on fragile class names or element selectors.

### The problem with child targeting

Traditional approaches to styling child components have significant limitations:

```tsx
// Relies on element types - breaks if implementation changes
<form className="[&_input]:rounded-lg [&_button]:mt-4" />

// Relies on class names - breaks if classes change
<form className="[&_.text-input]:rounded-lg" />

// Requires passing classes through props - verbose
<form>
  <input className={inputClasses} />
  <button className={buttonClasses} />
</form>
```

### The solution: data-slot attributes

Use `data-slot` to give components stable identifiers that can be targeted by parents:

```tsx title="field-set.tsx"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
  return (
    <fieldset
      data-slot="field-set"
      className={cn(
        "flex flex-col gap-6",
        // Target specific child slots
        "has-[>[data-slot=checkbox-group]]:gap-3",
        "has-[>[data-slot=radio-group]]:gap-3",
        className,
      )}
      {...props}
    />
  );
}
```

```tsx title="checkbox-group.tsx"
function CheckboxGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="checkbox-group"
      className={cn("flex flex-col gap-2", className)}
      {...props}
    />
  );
}
```

### Benefits of data-slot

1. **Stable identifiers** - Won't break when implementation details change
2. **Semantic targeting** - Target based on component purpose, not structure
3. **Encapsulation** - Internal classes remain private
4. **Composable** - Works with arbitrary nesting and composition
5. **Type-safe** - Can be validated and documented

### Using `has-[]` for parent-aware styling

Tailwind's `has-[]` selector combined with `data-slot` creates powerful parent-aware styling:

```tsx title="form.tsx"
function Form({ className, ...props }: React.ComponentProps<"form">) {
  return (
    <form
      data-slot="form"
      className={cn(
        "space-y-4",
        // Adjust spacing when specific slots are present
        "has-[>[data-slot=form-section]]:space-y-6",
        "has-[>[data-slot=inline-fields]]:space-y-2",
        // Style based on slot states
        "has-[[data-slot=submit-button][data-loading=true]]:opacity-50",
        className,
      )}
      {...props}
    />
  );
}
```

### Using `[&_]` for descendant targeting

For deeper nesting, use the `[&_selector]` pattern to target any descendant:

```tsx title="card.tsx"
function Card({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card"
      className={cn(
        "rounded-lg border p-4",
        // Target any descendant with data-slot
        "[&_[data-slot=card-header]]:mb-4",
        "[&_[data-slot=card-title]]:text-lg [&_[data-slot=card-title]]:font-semibold",
        "[&_[data-slot=card-description]]:text-sm [&_[data-slot=card-description]]:text-muted-foreground",
        "[&_[data-slot=card-footer]]:mt-4 [&_[data-slot=card-footer]]:border-t [&_[data-slot=card-footer]]:pt-4",
        className,
      )}
      {...props}
    />
  );
}
```

### Global CSS with data-slot

Data slots work beautifully with global CSS for theme-wide consistency:

```css title="globals.css"
/* Style all buttons within forms */
[data-slot="form"] [data-slot="button"] {
  @apply w-full sm:w-auto;
}

/* Style submit buttons specifically */
[data-slot="form"] [data-slot="submit-button"] {
  @apply bg-primary text-primary-foreground;
}

/* Adjust inputs within inline layouts */
[data-slot="inline-fields"] [data-slot="input"] {
  @apply flex-1;
}

/* Style based on state combinations */
[data-slot="dialog"][data-state="open"] [data-slot="dialog-content"] {
  @apply animate-in fade-in;
}
```

### Naming conventions

Follow these conventions for consistent `data-slot` naming:

1. **Use kebab-case** - `data-slot="form-field"` not `data-slot="formField"`
2. **Be specific** - `data-slot="submit-button"` not `data-slot="button"`
3. **Match component purpose** - Name reflects what it does, not how it looks
4. **Avoid implementation details** - `data-slot="user-avatar"` not `data-slot="rounded-image"`

```tsx
// Good examples
data-slot="search-input"
data-slot="navigation-menu"
data-slot="error-message"
data-slot="submit-button"
data-slot="card-header"

// Avoid
data-slot="input"           // Too generic
data-slot="blueButton"      // Includes styling
data-slot="div-wrapper"     // Implementation detail
data-slot="mainContent"     // Use camelCase
```

## When to use data attributes vs props

Understanding when to use each pattern is key to a clean API:

### `data-state` use cases

* **Visual states** - open/closed, active/inactive, loading, etc.
* **Layout states** - orientation, side, alignment
* **Interaction states** - hover, focus, disabled (when you need to style children)

### `data-slot` use cases

* **Component identification** - Stable identifiers for targeting
* **Composition patterns** - Parent-child relationships
* **Global styling** - Theme-wide component styling
* **Variant-independent targeting** - Target any variant of a component

### `props` use cases

* **Variants** - Different visual designs (primary, secondary, destructive)
* **Sizes** - sm, md, lg
* **Behavioral configuration** - controlled/uncontrolled, default values
* **Event handlers** - onClick, onChange, etc.

### Combined approach

A well-designed component uses all three patterns appropriately:

```tsx title="button.tsx"
type ButtonProps = {
  variant?: "primary" | "secondary" | "destructive";
  size?: "sm" | "md" | "lg";
  loading?: boolean;
  disabled?: boolean;
  onClick?: () => void;
  className?: string;
};

const Button = ({
  variant = "primary",
  size = "md",
  loading,
  disabled,
  className,
  ...props
}: ButtonProps) => {
  return (
    <button
      // Slot for targeting
      data-slot="button"
      // State for conditional styling
      data-loading={loading}
      data-disabled={disabled}
      className={cn(
        // Variant styles via props
        buttonVariants({ variant, size }),
        // Additional state styling allowed via className
        className,
      )}
      disabled={disabled}
      {...props}
    />
  );
};
```

Now the button can be used and styled in multiple ways:

```tsx
// Basic usage with variants
<Button variant="primary" size="lg">Submit</Button>

// Parent targeting via data-slot
<form className="[&_[data-slot=button]]:w-full">
  <Button>Submit</Button>
</form>

// State-based styling via data-state
<Button
  loading={isLoading}
  className="data-[loading=true]:opacity-50"
>
  Submit
</Button>

// Global CSS can target any button
// [data-slot="button"][data-loading="true"] { ... }
```

Data attributes provide a robust foundation for styling modern component libraries. By using `data-state` for visual states and `data-slot` for component identification, you create a flexible, maintainable API that scales from simple components to complex design systems.


---
title: Definitions
description: This page establishes precise terminology used throughout the specification. Terms are intentionally framework agnostic, but we will use React for examples.
type: reference
summary: Precise terminology for the specification covering primitives, components, patterns, blocks, pages, templates, and utilities.
related:
  - /principles
  - /composition
---

# Definitions



## 1. Artifact Taxonomy

### 1.1 Primitive

A primitive (or, unstyled component) is the **lowest‑level building block** that provides behavior and accessibility without any styling.

Primitives are completely headless (i.e. unstyled) and encapsulate semantics, focus management, keyboard interaction, layering/portals, ARIA wiring, measurement, and similar concerns. They provide the behavioral foundation but require styling to become finished UI.

Examples:

* [Radix UI Primitives](https://www.radix-ui.com/primitives) (Dialog, Popover, Tooltip, etc.)
* [React Aria Components](https://react-spectrum.adobe.com/react-aria)
* [Base UI](https://base-ui.com)
* [Headless UI](https://headlessui.com/)

Expectations:

* Completely unstyled (headless).
* Single responsibility; composable into styled components.
* Ships with exhaustive a11y behavior for its role.
* Versioning favors stability; breaking changes are rare and documented.

<Callout>
  The terms primitive and component are typically used interchangeably across
  the web, but they are not the same.
</Callout>

### 1.2 Component

A component is a styled, reusable UI unit that adds visual design to primitives or composes multiple elements to create complete, functional interface elements.

Components are still relatively low-level but include styling, making them immediately usable in applications. They typically wrap unstyled primitives with default visual design while remaining customizable.

Examples:

* [shadcn/ui components](https://ui.shadcn.com/) (styled wrappers of Radix primitives)
* [Material UI components](https://mui.com/components/)
* [Ant Design components](https://ant.design/components/overview/)

Expectations:

* Clear props API; supports controlled and uncontrolled usage where applicable.
* Includes default styling but remains override-friendly (classes, tokens, slots).
* Fully keyboard accessible and screen-reader friendly (inherits from primitives).
* Composable (children/slots, render props, or compound subcomponents).
* May be built from primitives or implement behavior directly with styling.

### 1.3 Pattern

Patterns are a specific composition of primitives or components that are used to solve a specific UI/UX problem.

Examples:

* Form validation with inline errors
* Confirming destructive actions
* Typeahead search
* Optimistic UI

Expectations.

* Describes behavior, a11y, keyboard map, and failure modes.
* May include reference implementations in multiple frameworks.

### 1.4 Block

An opinionated, production-ready composition of components that solves a concrete interface use case (often product-specific) with content scaffolding. Blocks trade generality for speed of adoption.

Examples:

* Pricing table
* Auth screens
* Onboarding stepper
* AI chat panel
* Billing settings form

Expectations.

* Strong defaults, copy-paste friendly, easily branded/themed.
* Minimal logic beyond layout and orchestration; domain logic is stubbed via handlers.
* Accepts data via props; never hides data behind fetches without a documented adapter.

<AuthorNote name="Rob Austin" role="Founder of shadcnblocks.com" githubUsername="JugglerX" link="https://www.shadcnblocks.com/">
  Blocks are typically not reusable like a component. You don't import them, but
  they typically import components and primitives. This makes them good
  candidates for a [Registry](/registry) distribution method.
</AuthorNote>

### 1.5 Page

A complete, single-route view composed of multiple blocks arranged to serve a specific user-facing purpose. Pages combine blocks into a cohesive layout that represents one destination in an application.

Examples:

* Landing page (hero block + features block + pricing block + footer block)
* Product detail page (image gallery block + product info block + reviews block)
* Dashboard page (stats block + chart block + activity feed block)

Expectations:

* Combines multiple blocks into a unified layout for a single route.
* Focuses on layout and block orchestration rather than component-level details.
* May include page-specific logic for data coordination between blocks.
* Self-contained for a single URL/route; not intended to be reused across routes.

### 1.6 Template

A multi-page collection or full-site scaffold that bundles pages, routing configuration, shared layouts, global providers, and project structure. Templates are complete starting points for entire applications or major application sections.

Examples:

* [TailwindCSS Templates](https://tailwindui.com/templates)
* [shadcnblocks Templates](https://www.shadcnblocks.com/templates) (full application shells)
* "SaaS starter" (auth pages + dashboard pages + settings pages + marketing pages)
* "E-commerce template" (storefront + product pages + checkout flow + admin pages)

Expectations:

* Includes multiple pages with routing/navigation structure.
* Provides global configuration (theme providers, auth context, layout shells).
* Opinionated project structure with clear conventions.
* Designed as a comprehensive starting point; fork and customize rather than import as dependency.
* May include build configuration, deployment setup, and development tooling.

### 1.7 Utility (Non-visual)

A helper exported for developer ergonomics or composition; not rendered UI.

Examples:

* React hooks (useControllableState, useId)
* Class utilities
* Keybinding helpers
* Focus scopes

Expectations.

* Side-effect free (except where explicitly documented).
* Testable in isolation; supports tree-shaking.

## 2. API and Composition Vocabulary

### 2.1 Props API

The public configuration surface of a component. Props are stable, typed, and documented with defaults and a11y ramifications.

### 2.2 Children / Slots

Placeholders for caller-provided structure or content.

* Children (implicit slot). JSX between opening/closing tags.
* Named slots. Props like icon, footer, or `<Component.Slot>` subcomponents.
* Slot forwarding. Passing DOM attributes/className/refs through to the underlying element.

### 2.3 Render Prop (Function-as-Child)

A function child used to delegate rendering while the parent supplies state/data.

```tsx
<ParentComponent data={data}>
  {(item) => <ChildComponent key={item.id} {...item} />}
</ParentComponent>
```

Use when the parent must own data/behavior but the consumer must fully control markup.

### 2.4 Controlled vs. Uncontrolled

**Controlled** and **uncontrolled** are terms used to describe the state of a component.

**Controlled** components have their value driven by props, and typically emit an `onChange` event (source of truth is the parent). **Uncontrolled** components hold internal state; and may expose a `defaultValue` and imperative reset.

Many inputs should support both. Learn more about [controlled and uncontrolled state](/state).

### 2.5 Provider / Context

A top-level component that supplies shared state/configuration to a subtree (e.g., theme, locale, active tab id). Providers are explicitly documented with required placement.

### 2.6 Portal

Rendering UI outside the DOM hierarchy to manage layering/stacking context (e.g., modals, popovers, toasts), while preserving a11y (focus trap, aria-modal, inert background).

## 3. Styling and Theming Vocabulary

### 3.1 Headless

Implements behavior and accessibility without prescribing appearance. Requires the consumer to supply styling.

### 3.2 Styled

Ships with default visual design (CSS classes, inline styles, or tokens) but remains override-friendly (className merge, CSS vars, theming).

### 3.3 Variants

Discrete, documented style or behavior permutations exposed via props (e.g., `size="sm|md|lg"`, `tone="neutral|destructive"`). Variants are not separate components.

### 3.4 Design Tokens

Named, platform-agnostic values (e.g., `--color-bg`, `--radius-md`, `--space-2`) that parameterize visual design and support theming.

## 4. Accessibility Vocabulary

### 4.1 Role / State / Property

WAI-ARIA attributes that communicate semantics (`role="menu"`), state (`aria-checked`), and relationships (`aria-controls`, `aria-labelledby`).

### 4.2 Keyboard Map

The documented set of keyboard interactions for a widget (e.g., `Tab`, `Arrow keys`, `Home/End`, `Escape`). Every interactive component declares and implements a keyboard map.

### 4.3 Focus Management

Rules for initial focus, roving focus, focus trapping, and focus return on teardown.

## 5. Distribution Vocabulary

### 5.1 Package (Registry Distribution)

The component/library is published to a package registry (e.g., `npm`) and imported via a bundler. Favors versioned updates and dependency management.

### 5.2 Copy-and-Paste (Source Distribution)

Source code is integrated directly into the consumer's repository (often via a CLI). Favors ownership, customization, and zero extraneous runtime.

### 5.3 Registry (Catalog)

A curated index of artifacts (primitives, components, blocks, templates) with metadata, previews, and install/copy instructions. A registry is not necessarily a package manager.

## 6. Classification Heuristics

Use this decision flow to name and place an artifact:

1. Does it encapsulate a single behavior or a11y concern, with no styling? → **Primitive**
2. Is it a styled, reusable UI element that adds visual design to primitives or composes multiple elements? → **Component**
3. Does it solve a concrete product use case with opinionated composition and copy? → **Block**
4. Does it scaffold a page/flow with routing/providers and replaceable regions? → **Template**
5. Is it documentation of a recurring solution, independent of implementation? → **Pattern**
6. Is it non-visual logic for ergonomics/composition? → **Utility**

## 7. Non-Goals and Clarifications

* Web Components vs. "Components." In this spec, "component" refers to a reusable UI unit (examples in React). It does not imply the HTML Custom Elements standard unless explicitly stated. Equivalent principles apply across frameworks.
* Widgets. The term “widget” is avoided due to ambiguity; use component (general) or pattern (documentation-only solution).
* Themes vs. Styles. A theme is a parameterization of styles (via tokens). Styles are the concrete presentation. Components should support themes; blocks/templates may ship opinionated styles plus theming hooks.


---
title: Design Tokens
description: How semantic naming conventions and design tokens create a flexible, maintainable theming system.
type: conceptual
summary: Semantic CSS variable architecture for theming with layers of abstraction that separate what something is from how it looks.
prerequisites:
  - /styling
related:
  - /data-attributes
---

# Design Tokens



One of the core foundations of modern component libraries lies in their thoughtful approach to styling. Rather than hardcoding colors or creating rigid class systems, we can employ a semantic naming convention that separates the concerns of theme, context, and usage.

This semantic naming convention is known as design tokens. This architectural decision creates a maintainable, flexible system that scales beautifully across applications.

## The Philosophy of Semantic CSS Variables

Traditional CSS approaches often couple color values directly to their usage contexts, creating brittle systems that are difficult to maintain. Design tokens take a different approach by creating layers of abstraction that separate what something is from how it looks.

## Understanding the Variable Architecture

Let's examine how we can structure our CSS variables to create this flexible system:

```css title="globals.css"
@import "tailwindcss";
@import "tw-animate-css";

@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);
}
```

In the above example, we have four design tokens:

* `--background`, which is used for background colors (primarily the background of the page)
* `--foreground`, which is used for foreground colors (the general text color)
* `--primary`, which is used for primary colors (the main color of the brand)
* `--primary-foreground`, which is used for primary foreground colors (the text color, as seen against the primary color)


---
title: Docs
description: How to document your components.
type: guide
summary: Essential documentation sections for components including demos, installation, API reference, accessibility notes, and changelog.
prerequisites:
  - /composition
related:
  - /registry
  - /npm
  - /marketplaces
---

# Docs



Good documentation is essential for making your components accessible and easy to use. This guide outlines the key elements every component documentation page should include.

## Documentation Framework

To scale your documentation, you can use a documentation framework. There are many options available depending on your projects' language and project needs. Popular options include:

* [Fumadocs](https://fumadocs.dev/) - Fast, feature-rich documentation framework for Next.js
* [Nextra](https://nextra.site/) - Markdown-based documentation with built-in search and theming
* [Content Collections](https://content-collections.dev/) - Type-safe content management for documentation
* [Docusaurus](https://docusaurus.io/) - Feature-rich documentation sites with versioning support
* [VitePress](https://vitepress.dev/) - Vue-powered static site generator optimized for documentation

Preferably, your framework choice should support syntax highlighting, custom components and be generally well designed.

## Essential Documentation Sections

### Overview

Start with a brief introduction explaining what the component does and when to use it.

### Demo, Source Code, and Preview

For a great first impression for developers, you should include a demo that shows the component in action, as well as the code used to create the demo.

If you're using an open source [Registry](/registry), you can also include a preview of the source code that is used to create the component.

Use code blocks with syntax highlighting and copy-to-clipboard functionality. Consider using tabbed interfaces to switch between these views without cluttering the page.

### Installation

Include a clear instruction on how to install the component. Preferably this should be a single command you can copy and paste into your terminal.

If you're building on shadcn/ui, you can use the [shadcn CLI](https://ui.shadcn.com/docs/cli) to install the component e.g.

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npx shadcn@latest add <your-component-url>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm dlx shadcn@latest add <your-component-url>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dlx shadcn@latest add <your-component-url>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun x shadcn@latest add <your-component-url>
    ```
  </CodeBlockTab>
</CodeBlockTabs>

If you're publishing to a [Marketplace](/marketplaces), you can use the marketplace's CLI to install the component e.g.

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npx shadcn@latest add https://21st.dev/r/<your-author>/<your-component>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm dlx shadcn@latest add https://21st.dev/r/<your-author>/<your-component>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dlx shadcn@latest add https://21st.dev/r/<your-author>/<your-component>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun x shadcn@latest add https://21st.dev/r/<your-author>/<your-component>
    ```
  </CodeBlockTab>
</CodeBlockTabs>

If you're not using shadcn/ui but you are building a [Registry](/registry), you could build your own CLI to install the component, e.g.

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npx your-registry-cli@latest add <your-component-url>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm dlx your-registry-cli@latest add <your-component-url>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dlx your-registry-cli@latest add <your-component-url>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun x your-registry-cli@latest add <your-component-url>
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Lastly, if you're publishing to npm, you can use the npm CLI to install the component e.g.

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install <your-component-name>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add <your-component-name>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add <your-component-name>
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add <your-component-name>
    ```
  </CodeBlockTab>
</CodeBlockTabs>

<Callout>
  To show multiple installation options like we've done above, you can use
  something like Fumadocs' [`package-install`
  syntax](https://fumadocs.dev/docs/headless/mdx/install).
</Callout>

### Features

List the key features of your component to help users quickly understand its capabilities and advantages. For example:

* **Customizable** – Easily adjust styles, sizes, and behavior to fit your needs.
* **Accessible by default** – Follows best practices for keyboard navigation, ARIA roles, and screen reader support.
* **Composable** – Designed to work seamlessly with other components and patterns.
* **Type-safe** – Ships with comprehensive TypeScript types for maximum safety and autocomplete.
* **Theming support** – Integrates with your design tokens or theme system.
* **Lightweight** – Minimal dependencies and optimized for performance.
* **SSR/SSG ready** – Works with server-side and static rendering frameworks.
* **Well-documented** – Includes clear usage examples and API reference.

Tailor this list to your specific component. Highlight what makes it unique or especially useful for developers.

### Examples

Demonstrate the component's flexibility with practical examples:

* **Variants** - Different visual styles or sizes available
* **States** - Loading, disabled, error, or success states
* **Advanced Usage** - Complex scenarios and edge cases
* **Composition** - How the component works with other components
* **Responsive Behavior** - How it adapts to different screen sizes

Each example should include both the rendered output and the corresponding code.

### Props and API Reference

Document all available props, methods, and configuration options. Consider grouping related props together and highlighting commonly used ones. For each prop, include:

* **Name** - The prop identifier
* **Type** - TypeScript type definition
* **Default** - Default value if not specified
* **Required** - Whether the prop is mandatory
* **Description** - What the prop does and when to use it

<Callout>
  If you're using Fumadocs, you might consider using [Auto Type
  Table](https://fumadocs.dev/docs/ui/components/auto-type-table) to ensure
  accuracy and reduce maintenance burden.
</Callout>

### Accessibility

Document how your component supports accessibility features:

* Keyboard navigation patterns
* ARIA attributes and roles
* Screen reader support
* Focus management
* Color contrast considerations

### Changelog and Versioning

It can be useful to maintain a changelog on each component documentation page covering:

* Version numbers following semantic versioning
* New features and enhancements
* Bug fixes
* Breaking changes
* Migration guides for major version updates

Help users understand what changed between versions and how to upgrade safely. Include code examples showing before/after patterns for breaking changes.

<Callout>
  If your syntax highlighting framework supports it (like Shiki), you might want
  to use a [diff transformer
  notation](https://shiki.style/packages/transformers#transformernotationdiff)
  to show the changes between versions.
</Callout>

## Best Practices

* Keep documentation up-to-date with code changes
* Use real-world examples that solve actual problems
* Include common pitfalls and troubleshooting tips
* Provide performance considerations when relevant
* Link to related components and patterns
* Make all code examples runnable and tested


---
title: Overview
description: components.build is an open-source standard for building modern, composable and accessible UI components.
type: overview
summary: An open standard co-authored by Hayden Bleasel and shadcn for building composable, accessible UI components for the modern web.
related:
  - /definitions
  - /principles
---

# Overview



Modern web applications are built on reusable UI components and how we design, build, and share them is important. This specification aims to establish a formal, open standard for building open-source UI components for the modern web.

It is co-authored by <Author name="Hayden Bleasel" image="https://github.com/haydenbleasel.png" href="https://x.com/haydenbleasel" /> and <Author name="shadcn" image="https://github.com/shadcn.png" href="https://x.com/shadcn" />, with contributions from the open-source community and informed by popular projects in the React ecosystem.

The goal is to help open-source maintainers and senior front-end engineers create components that are composable, accessible, and easy to adopt across projects.

## What is this specification?

This spec is not a tutorial or course on React, nor a promotion for any specific component library or registry. Instead, it provides high-level guidelines, best practices, and a common terminology for designing UI components.

By following this specification, developers can ensure their components are consistent with modern expectations and can integrate smoothly into any codebase.

## Who is this for?

We're writing this for open-source maintainers and experienced front-end engineers who build and distribute component libraries or design systems. We assume you are familiar with JavaScript/TypeScript and React.

All examples will use React (with JSX/TSX) for concreteness, but we hope the fundamental concepts apply to other frameworks like Vue, Svelte, or Angular.

In other words, we hope this spec’s philosophy is framework-agnostic – whether you build with React or another library, you should emphasize the same principles of composition, accessibility, and maintainability.


---
title: Marketplaces
description: Using component marketplaces to share your components.
type: conceptual
summary: Centralized platforms like 21st.dev for publishing, discovering, and installing UI components with built-in previews and community feedback.
prerequisites:
  - /registry
related:
  - /npm
  - /docs
---

# Marketplaces



Component marketplaces represent another new paradigm in how developers share and discover UI components. Platforms like [21st.dev](https://21st.dev) have emerged as centralized hubs where creators can publish components and consumers can discover, preview, and install them seamlessly.

These marketplaces combine the accessibility of registries with the discoverability of traditional package repositories, creating vibrant ecosystems for component sharing.

## How Marketplaces Work

When you publish to a marketplace, the platform typically:

1. **Hosts your component code** - No need to manage your own infrastructure
2. **Provides a unified CLI** - Users install components through the marketplace's tooling
3. **Generates previews** - Live demos and interactive examples are created automatically
4. **Handles discovery** - Search, categories, and recommendations surface relevant components

For example, installing from 21st.dev uses the familiar shadcn CLI pattern:

```bash title="Terminal"
npx shadcn@latest add https://21st.dev/r/haydenbleasel/dialog-stack
```

The marketplace handles serving the component metadata and source code, making the installation process seamless.

## Benefits of Component Marketplaces

Marketplaces offer unique advantages that neither registries nor npm packages can match on their own.

### For Component Authors

#### Distribution Without Infrastructure

Publishing to a marketplace eliminates infrastructure concerns. You don't need to setup hosting for your components, or manage CDN distribution.

The marketplace handles all of this, letting you focus on creating great components.

#### Built-in Audience

Marketplaces aggregate demand. When developers visit 21st.dev or similar platforms, they're actively looking for components. Your work gets discovered organically through search, categories, and recommendations.

This built-in traffic means you don't need to market your components independently.

#### Monetization Opportunities

Some marketplaces enable creators to monetize their work through premium component tiers, sponsorship programs, usage-based pricing, and one-time purchase models.

This creates sustainable incentives for maintaining high-quality components.

#### Community Feedback

Marketplaces typically include rating systems, comment sections, usage analytics, and issue tracking.

This feedback loop helps you understand how developers use your components and what improvements they need.

### For Component Consumers

#### Curated Discovery

Unlike searching through GitHub or npm, marketplaces offer curated experiences:

```
Browse by category:
├── Marketing
│   ├── Heroes
│   ├── Pricing
│   └── Testimonials
├── Application
│   ├── Dashboards
│   ├── Forms
│   └── Data Display
└── E-commerce
    ├── Product Cards
    ├── Cart
    └── Checkout
```

This organization makes finding the right component significantly faster.

#### Quality Assurance

Many marketplaces implement review processes. Components might go through code quality checks, accessibility audits, performance testing, and documentation review.

This curation ensures a baseline quality that self-published components might lack.

#### Unified Tooling

Using a single CLI for all marketplace components simplifies workflow. You can install any component from the marketplace using the same command:

```bash title="Terminal"
npx shadcn@latest add https://21st.dev/r/<author>/<component>
```

## Challenges of Marketplaces

While marketplaces offer compelling benefits, they also introduce unique challenges.

### For Authors

#### Competition and Visibility

In a crowded marketplace, standing out becomes difficult. You're competing with hundreds of similar components, established creators with followings, featured or sponsored content, and algorithm-driven recommendations.

Success requires more than just good code - you need compelling previews, excellent documentation, and often, active promotion.

#### Platform Dependency

Publishing on a marketplace means accepting their terms of service, revenue sharing models, technical requirements, and review processes.

If the platform changes policies or shuts down, your distribution channel disappears.

#### Quality Pressure

Marketplace users expect polished, production-ready components. This means comprehensive documentation, multiple demo variations, responsive design, cross-browser testing, and accessibility compliance.

Meeting these expectations requires significant time investment beyond just writing component code.

### For Consumers

#### Variable Quality

Despite review processes, marketplace quality varies wildly. You might encounter abandoned components with outdated dependencies, poorly documented code, components that work in demos but break in production, and inconsistent coding styles across different authors.

Due diligence is still required when selecting components.

#### Lock-in Concerns

While marketplaces use standard tools like the shadcn CLI, they might introduce proprietary metadata formats, custom configuration requirements, and platform-specific dependencies.

This can make migrating away from the marketplace difficult.

#### Discovery Paradox

Too much choice can be paralyzing. When searching for a button component, finding 200 options doesn't necessarily help. You need to evaluate multiple similar components, compare subtle differences, assess long-term maintainability, and consider author reputation.

This evaluation process can take longer than building the component yourself.


---
title: NPM
description: How to publish your components to NPM.
type: guide
summary: Publishing component libraries to npm with versioned distribution, Tailwind configuration, TypeScript support, and package.json setup.
prerequisites:
  - /registry
related:
  - /marketplaces
  - /docs
---

# NPM



NPM packages represent the traditional approach to distributing component libraries. While [registries](/registry) have gained popularity for their flexibility, npm publishing remains a powerful option with distinct advantages for certain use cases.

The fundamental difference between npm packages and registries lies in how they distribute code and manage ownership.

## Package Model

When you publish components as an npm package, you're distributing pre-built, versioned code that users install as a dependency:

```bash title="Terminal"
npm install @acme/ui-components
```

```tsx title="MyApp.tsx"
import { Button } from "@acme/ui-components";

// Component is imported from node_modules
// Source code is not directly editable
```

This offers several compelling advantages that make them the right choice for many component libraries.

### Version Management

As the package author, you control versioning and updates. Users can lock to specific versions, ensuring stability:

```json
{
  "dependencies": {
    "@acme/ui-components": "^2.1.0"
  }
}
```

This centralized version control means you can push updates, security patches, and new features that users receive through standard dependency updates.

### Simplified Installation

NPM packages provide a frictionless installation experience. A single command adds your entire component library:

```bash
npm install @acme/ui-components
```

No need to manually copy files, manage dependencies, or configure build tools. Everything works out of the box.

### Dependency Resolution

NPM automatically handles transitive dependencies. If your components require specific versions of React, Framer Motion, or other libraries, npm resolves these dependencies automatically, preventing version conflicts.

### TypeScript Support

Published packages can include pre-built type definitions, providing immediate TypeScript support without additional configuration:

```json
{
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  }
}
```

## Limitations of NPM Packages

While npm packages excel in distribution, they come with trade-offs that registries specifically address.

### Source Code Ownership

The most significant limitation is the lack of source code access. Users cannot:

* Modify component behavior directly
* Fix bugs without waiting for updates
* Customize implementation details
* Remove unused code

This creates a dependency relationship where users must rely on the package maintainer for all changes.

### Customization Constraints

Tweaking components requires working within the exposed API. While you can provide props for customization:

```tsx
<Button variant="primary" size="large" className="custom-styles" />
```

Users cannot fundamentally alter how the component works without forking the entire package.

### Bundle Size

NPM packages include all components, even if users only need a subset. While tree-shaking helps, it's not always perfect, potentially adding unnecessary weight to applications.

## CSS and Tailwind Configuration

One critical consideration when publishing Tailwind-based components via npm is ensuring styles work correctly in the consuming application.

By default, Tailwind only generates styles for classes it finds in your project files. It doesn't look inside `node_modules`, which means your component styles won't be included.

To fix this, users need to add a `@source` directive to their Tailwind configuration, telling it to scan your package for class names:

```css title="globals.css"
@import "tailwindcss";

/* Tell Tailwind to look for classes in your package */
@source "../node_modules/@acme/ui-components";
```

Always document this requirement prominently in your package README.

## Publishing Your Component Library

To publish your components to npm, you need a properly configured `package.json` which might look like this:

```json title="package.json"
{
  "name": "@acme/ui-components",
  "version": "1.0.0",
  "description": "A collection of 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"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.0.0"
  }
}
```

NPM packages remain a vital part of the component ecosystem. While registries offer compelling benefits for source code ownership and customization, npm packages provide stability, version management, and ease of use that many teams require.

The key is understanding your users' needs and choosing the distribution method that best serves them. Sometimes, that means offering both options and letting developers choose what works best for their project.


---
title: Polymorphism
description: How to use the `as` prop to change the rendered HTML element while preserving component functionality.
type: reference
summary: The as prop pattern for polymorphic components, compared with Radix UI's Slot-based asChild approach with TypeScript best practices.
prerequisites:
  - /composition
  - /types
related:
  - /as-child
  - /accessibility
---

# Polymorphism



The `as` prop is a fundamental pattern in modern React component libraries that allows you to change the underlying HTML element or component that gets rendered.

Popularized by libraries like [Styled Components](https://styled-components.com/), [Emotion](https://emotion.sh/), and [Chakra UI](https://chakra-ui.com/), this pattern provides flexibility in choosing semantic HTML while maintaining component styling and behavior.

The `as` prop enables polymorphic components - components that can render as different element types while preserving their core functionality:

```tsx
<Button as="a" href="/home">
  Go Home
</Button>

<Button as="button" type="submit">
  Submit Form
</Button>

<Button as="div" role="button" tabIndex={0}>
  Custom Element
</Button>
```

## Understanding `as`

The `as` prop allows you to override the default element type of a component. Instead of being locked into a specific HTML element, you can adapt the component to render as any valid HTML tag or even another React component.

For example:

```tsx
// Default renders as a div
<Box>Content</Box>

// Renders as a section
<Box as="section">Content</Box>

// Renders as a nav
<Box as="nav">Content</Box>
```

This renders different HTML elements:

```html
<!-- Default -->
<div>Content</div>

<!-- With as="section" -->
<section>Content</section>

<!-- With as="nav" -->
<nav>Content</nav>
```

## Implementation Methods

There are two main approaches to implementing polymorphic components: a manual implementation and using Radix UI's `Slot` component.

### Manual Implementation

The `as` prop implementation uses dynamic component rendering:

```tsx
// Simplified implementation
function Component({ as: Element = "div", children, ...props }) {
  return <Element {...props}>{children}</Element>;
}

// More complete implementation with TypeScript
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>;
}
```

The component:

1. Accepts an `as` prop with a default element type
2. Uses the provided element or fallback to default
3. Spreads all other props to the rendered element
4. Maintains type safety with TypeScript generics

### Using Radix UI Slot

[Radix UI](https://www.radix-ui.com/) provides a `Slot` component that offers a more powerful alternative to the `as` prop pattern. Instead of just changing the element type, `Slot` merges props with the child component, enabling composition patterns.

First, install the package:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @radix-ui/react-slot
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @radix-ui/react-slot
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @radix-ui/react-slot
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @radix-ui/react-slot
    ```
  </CodeBlockTab>
</CodeBlockTabs>

The `asChild` pattern uses a boolean prop instead of specifying the element type:

```tsx
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

const itemVariants = cva("rounded-lg border p-4", {
  variants: {
    variant: {
      default: "bg-white",
      primary: "bg-blue-500 text-white",
    },
    size: {
      default: "h-10 px-4",
      sm: "h-8 px-3",
      lg: "h-12 px-6",
    },
  },
  defaultVariants: {
    variant: "default",
    size: "default",
  },
});

function Item({
  className,
  variant = "default",
  size = "default",
  asChild = false,
  ...props
}: React.ComponentProps<"div"> &
  VariantProps<typeof itemVariants> & { asChild?: boolean }) {
  const Comp = asChild ? Slot : "div";
  return (
    <Comp
      data-slot="item"
      data-variant={variant}
      data-size={size}
      className={cn(itemVariants({ variant, size, className }))}
      {...props}
    />
  );
}
```

Now you can use it in two ways:

```tsx
// Default: renders as a div
<Item variant="primary">Content</Item>

// With asChild: merges props with child component
<Item variant="primary" asChild>
  <a href="/home">Link with Item styles</a>
</Item>
```

The `Slot` component:

1. Clones the child element
2. Merges the component's props (className, data attributes, etc.) with the child's props
3. Forwards refs correctly
4. Handles event handler composition

### Comparison: `as` vs `asChild`

**`as` prop (manual implementation):**

```tsx
// Explicit element type
<Button as="a" href="/home">Link Button</Button>
<Button as="button" type="submit">Submit Button</Button>

// Simple, predictable API
// Limited to element types
```

**`asChild` with Slot:**

```tsx
// Implicit from child
<Button asChild>
  <a href="/home">Link Button</a>
</Button>

<Button asChild>
  <button type="submit">Submit Button</button>
</Button>

// More flexible composition
// Works with any component
// Better prop merging
```

**Key differences:**

| Feature                   | `as` prop           | `asChild` + Slot                 |
| ------------------------- | ------------------- | -------------------------------- |
| **API Style**             | `<Button as="a">`   | `<Button asChild><a /></Button>` |
| **Element Type**          | Specified in prop   | Inferred from child              |
| **Component Composition** | Limited             | Full support                     |
| **Prop Merging**          | Basic spread        | Intelligent merging              |
| **Ref Forwarding**        | Manual setup needed | Built-in                         |
| **Event Handlers**        | May conflict        | Composed correctly               |
| **Library Size**          | No dependency       | Requires `@radix-ui/react-slot`  |

### When to Use Each Approach

**Use `as` prop when:**

* You want a simpler API surface
* You're primarily switching between HTML elements
* You want to avoid additional dependencies
* The component is simple and doesn't need complex prop merging

**Use `asChild` + Slot when:**

* You need to compose with other components
* You want automatic prop merging behavior
* You're building a component library similar to Radix UI or shadcn/ui
* You need reliable ref forwarding across different component types

## Key Benefits

### 1. Semantic HTML Flexibility

The `as` prop ensures you can always use the most semantically appropriate HTML element:

```tsx
// Navigation container
<Container as="nav" className="navigation">
  <NavItems />
</Container>

// Main content area
<Container as="main" className="content">
  <Article />
</Container>

// Sidebar
<Container as="aside" className="sidebar">
  <Widgets />
</Container>
```

### 2. Component Reusability

One component can serve multiple purposes without creating variants:

```tsx
// Text component used for different elements
<Text as="h1" size="2xl">Page Title</Text>
<Text as="p" size="md">Body paragraph</Text>
<Text as="span" size="sm">Inline text</Text>
<Text as="label" size="sm">Form label</Text>
```

### 3. Accessibility Improvements

Choose elements that provide the best accessibility for each context:

```tsx
// Link that looks like a button
<Button as="a" href="/signup">
  Sign Up Now
</Button>

// Button that submits a form
<Button as="button" type="submit">
  Submit
</Button>

// Heading with button styles
<Button as="h2" role="presentation">
  Section Title
</Button>
```

### 4. Style System Integration

Maintain consistent styling while changing elements:

```tsx
const Card = styled.div`
  padding: 1rem;
  border-radius: 8px;
  background: white;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
`;

// Same styles, different elements
<Card as="article">Article content</Card>
<Card as="section">Section content</Card>
<Card as="li">List item content</Card>
```

## Common Use Cases

### Typography Components

Create flexible text components:

```tsx
function Text({
  as: Element = 'span',
  variant = 'body',
  ...props
}) {
  const className = cn(
    'text-base',
    variant === 'heading' && 'text-2xl font-bold',
    variant === 'body' && 'text-base',
    variant === 'caption' && 'text-sm text-gray-600',
    props.className
  );

  return <Element className={className} {...props} />;
}

// Usage
<Text as="h1" variant="heading">Title</Text>
<Text as="p" variant="body">Paragraph</Text>
<Text as="figcaption" variant="caption">Caption</Text>
```

### Layout Components

Build semantic layouts:

```tsx
function Flex({ as: Element = 'div', ...props }) {
  return (
    <Element
      className={cn('flex', props.className)}
      {...props}
    />
  );
}

// Semantic HTML
<Flex as="header" className="justify-between">
  <Logo />
  <Navigation />
</Flex>

<Flex as="main" className="flex-col">
  <Content />
</Flex>
```

### Interactive Elements

Handle different interaction types:

```tsx
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}
    />
  );
}

// Various clickable elements
<Clickable as="button" onClick={handleClick}>Button</Clickable>
<Clickable as="a" href="/link">Link</Clickable>
<Clickable as="div" onClick={handleClick}>Div Button</Clickable>
```

## TypeScript Best Practices

### Generic Component Types

Create fully type-safe polymorphic components:

```tsx
type PolymorphicRef<E extends React.ElementType> =
  React.ComponentPropsWithRef<E>["ref"];

type PolymorphicProps<E extends React.ElementType, Props = {}> = Props &
  Omit<React.ComponentPropsWithoutRef<E>, keyof Props> & {
    as?: E;
  };

// Component with full type safety
function Component<E extends React.ElementType = "div">({
  as,
  ...props
}: PolymorphicProps<E, { customProp?: string }>) {
  const Element = as || "div";
  return <Element {...props} />;
}
```

### Inferring Props

Automatically infer props based on the element:

```tsx
// Props are inferred from the element type
<Component as="a" href="/home">Home</Component>  // ✅ href is valid
<Component as="div" href="/home">Home</Component> // ❌ TS error: href not valid on div

<Component as="button" type="submit">Submit</Component> // ✅ type is valid
<Component as="span" type="submit">Submit</Component>   // ❌ TS error
```

### Discriminated Unions

Use discriminated unions for element-specific props:

```tsx
type ButtonProps =
  | { as: "button"; type?: "submit" | "button" | "reset" }
  | { as: "a"; href: string; target?: string }
  | { as: "div"; role: "button"; tabIndex: number };

function Button(props: ButtonProps & { children: React.ReactNode }) {
  const Element = props.as;
  return <Element {...props} />;
}
```

## Best Practices

### 1. Default to Semantic Elements

Choose meaningful defaults that represent the most common use case:

```tsx
// ✅ Good defaults
function Article({ as: Element = "article", ...props }) {}
function Navigation({ as: Element = "nav", ...props }) {}
function Heading({ as: Element = "h2", ...props }) {}

// ❌ Too generic
function Component({ as: Element = "div", ...props }) {}
```

### 2. Document Valid Elements

Clearly specify which elements are supported:

```tsx
interface BoxProps {
  /**
   * The HTML element to render as
   * @default 'div'
   * @example 'section', 'article', 'aside', 'main'
   */
  as?: "div" | "section" | "article" | "aside" | "main" | "header" | "footer";
}
```

### 3. Validate Element Appropriateness

Warn when inappropriate elements are used:

```tsx
function Button({ as: Element = "button", ...props }) {
  if (__DEV__ && Element === "div" && !props.role) {
    console.warn(
      'Button: When using as="div", provide role="button" for accessibility',
    );
  }

  return <Element {...props} />;
}
```

### 4. Handle Event Handlers Properly

Ensure event handlers work across different elements:

```tsx
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

Be careful about HTML nesting rules:

```tsx
// ❌ Invalid - button inside button
<Button as="button">
  <Button as="button">Nested</Button>
</Button>

// ❌ Invalid - div inside p
<Text as="p">
  <Box as="div">Invalid nesting</Box>
</Text>

// ✅ Valid nesting
<Text as="div">
  <Box as="div">Valid nesting</Box>
</Text>
```

### Missing Accessibility Attributes

Remember to add appropriate ARIA attributes:

```tsx
// ❌ Missing accessibility
<Box as="nav">
  <MenuItems />
</Box>

// ✅ Proper accessibility
<Box as="nav" aria-label="Main navigation">
  <MenuItems />
</Box>
```

### Type Safety Loss

Avoid using overly permissive types:

```tsx
// ❌ Too permissive - no type safety
function Component({ as: Element = "div", ...props }: any) {
  return <Element {...props} />;
}

// ✅ Type safe
function Component<E extends React.ElementType = "div">({
  as,
  ...props
}: PolymorphicProps<E>) {
  const Element = as || "div";
  return <Element {...props} />;
}
```

### Performance Considerations

Be aware of re-render implications:

```tsx
// ❌ 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} />;
}
```


---
title: Core Principles
description: When building modern UI components, it's important to keep these core principles in mind.
type: conceptual
summary: The foundational values behind the specification including composability, accessibility, customizability, performance, and transparency.
prerequisites:
  - /definitions
related:
  - /composition
  - /accessibility
---

# Core Principles



## Composability and Reusability

Favor composition over inheritance – build components that can be combined and nested to create more complex UIs, rather than relying on deep class hierarchies.

Composable components expose a clear API (via props/slots) that allows developers to customize behavior and appearance by plugging in child elements or callbacks.

This makes components highly reusable in different contexts. (React’s design reinforces this: “Props and composition give you all the flexibility you need to customize a component’s look and behavior in an explicit and safe way.”)

## Accessible by Default

Components must be usable by all users. Use semantic HTML elements appropriate to the component’s role (e.g. `<button>` for clickable actions, `<ul>/<li>` for lists, etc.) and augment with WAI-ARIA attributes when necessary.

Ensure keyboard navigation and focus management are supported (for example, arrow-key navigation in menus, focus traps in modals). Each component should adhere to accessibility standards and guidelines out of the box.

This means providing proper ARIA roles/states and testing with screen readers. Accessibility is not optional – it’s a baseline feature of every component.

## Customizability and Theming

A component should be easy to restyle or adapt to different design requirements. Avoid hard-coding visual styles that cannot be overridden.

Provide mechanisms for theming and styling, such as CSS variables, clearly documented class names, or style props. Ideally, components come with sensible default styling but allow developers to customize appearance with minimal effort (for example, by passing a className or using design tokens).

This principle ensures components can fit into any brand or design system without “fighting” against default styles.

## Lightweight and Performant

Components should be as lean as possible in terms of assets and dependencies. Avoid bloating a component with large library dependencies or overly complex logic, especially if that logic isn’t always needed.

Strive for good performance (both rendering and interaction) by minimizing unnecessary re-renders and using efficient algorithms for heavy tasks. If a component is data-intensive (like a large list or table), consider patterns like virtualization or incremental rendering, but keep such features optional.

Lightweight components are easier to maintain and faster for end users.

## Transparency and Code Ownership

In open-source, consumers often benefit from having full visibility and control of component code. This spec encourages an “open-source first” mindset: components should not be black boxes.

When developers import or copy your component, they should be able to inspect how it works and modify it if needed. This principle underlies the emerging “copy-and-paste” distribution model (discussed later) where developers integrate component code directly into their projects.

By giving users ownership of the code, you increase trust and allow deeper customization.

Even if you distribute via a package, embrace transparency by providing source maps, readable code, and thorough documentation.

## Well-documented and DX-Friendly

A great component is not just code – it comes with clear documentation and examples. From a developer experience (DX) perspective, your components should be easy to learn and integrate.

Document each component’s purpose, props, and usage examples. Include notes on accessibility (like keyboard controls or ARIA attributes used) and any customization options.

Good documentation reduces misuse and lowers the barrier for adoption. We will cover documentation expectations in the Publish section, but it’s listed here as a principle because planning for good documentation and DX should happen during the design/build phase.


---
title: Registry
description: Understand the concept of component registries, how they work, and why they're revolutionizing how developers share and discover UI components.
type: conceptual
summary: What component registries are, their source-code distribution model, and how to create one using the shadcn ecosystem.
related:
  - /npm
  - /marketplaces
  - /docs
---

# Registry



Component registries are a way to share and discover UI components. Popularized by [shadcn/ui](https://ui.shadcn.com), they allow you to discover and copy components directly into your projects.

Registries represent a fundamental shift in how developers share and discover UI components. Unlike traditional npm packages, registries rely on an open source model and work through downloading the source code to your project.

## What Makes a Registry?

### 1. Source Code Distribution

Unlike npm packages that distribute compiled code, registries distribute source code:

```typescript
// Traditional npm package
import { Button } from "some-ui-library";

// Registry-based component
// Copy source from registry into your project
// src/components/ui/button.tsx contains the full source
import { Button } from "@/components/ui/button";
```

### 2. Metadata and Configuration

Good registries include rich metadata about components like the name, description, dependencies, and category.

```json
{
  "name": "announcement",
  "type": "registry:component",
  "description": "A compound badge component designed to display announcements with theming support",
  "dependencies": ["class-variance-authority", "lucide-react"],
  "registryDependencies": ["badge"],
  "files": [
    {
      "type": "registry:component",
      "content": "..."
    }
  ],
  "category": "ui"
}
```

### 3. Preview and Documentation

While not downloaded, registry websites typically provide:

* Live component previews
* Interactive examples
* Detailed documentation
* Code snippets ready to copy

## Registry Architecture Benefits

Component registries offer significant advantages for both authors and users, streamlining the process of sharing and adopting UI components.

### For Authors

For component authors, registries make distribution remarkably simple. Once a component is created, it can be added to the registry, making it instantly accessible to users without the need for complex publishing steps. This ease of distribution accelerates the feedback loop and encourages rapid iteration.

Version control is another key benefit. Registries typically track component versions, changelogs, and compatibility information. For example, a component entry might specify its current version, highlight recent changes such as improved accessibility or new features, and indicate which versions of shadcn/ui it supports. This transparency helps maintainers communicate updates and ensures users can select components that fit their project requirements.

Community engagement is also enhanced through registries. Authors can receive direct feedback from users, who are able to report issues, request features, and contribute to collaborative improvements. This fosters a more interactive and responsive development environment, benefiting both creators and consumers.

### For Consumers

From the perspective of component users, registries greatly improve the discovery process. Users can browse components by category, utilize search functionality, view popularity metrics, and explore related components, making it easier to find exactly what they need for their projects.

Before integrating a component, users can preview it in action, experiment with different variants, and review its behavior and code quality. This ability to evaluate components beforehand reduces risk and increases confidence in adoption.

Perhaps most importantly, registries empower users with true ownership. Instead of being locked into a dependency, users copy the source code directly into their projects. This means they can modify components as needed, avoid dependency management headaches, and retain full control over their codebase.

## Creating a Registry

You can create a simple registry quite quickly. Practically speaking, you only need 3 core elements:

### 1. Components

Create a component, or set of components, that you want to share. Make sure you have the source code for the components, and that they are well-documented and easy to understand.

Consider adding things like Markdown documentation, example implementations, and a way to preview the component.

### 2. A public endpoint

Create a public endpoint that serves the components. This can be a simple JSON file, or a more complex website. As long as it is public and accessible, you can use any endpoint you want.

### 3. CLI

Create a CLI that allows you to install the components into your project. This can be as simple as a single command, like `npx myregistry add button`, or a more complex command with options and flags.

## Using the shadcn Registry

Building your own registry is a fantastic way to share your components with the community, but it requires a lot of effort and maintenance. If you just want to share a component or two, you can use the shadcn/ui ecosystem - registry, CLI and variables.

Let's see how we can publish a `MetricCard` component live in less than 5 minutes using [Vercel](https://vercel.com)'s static hosting.

### Step 1: Create a Folder

Make a folder with this structure:

```
my-component/
├── public/
│   └── metric-card.json
└── vercel.json
```

Put your registry item JSON (e.g. `metric-card.json`) in the `public/` folder.

### Step 2: Add a `vercel.json`

Create a `vercel.json` file next to `public/` with the following:

```json title="vercel.json"
{
  "headers": [
    {
      "source": "/(.*).json",
      "headers": [
        {
          "key": "Access-Control-Allow-Origin",
          "value": "*"
        },
        {
          "key": "Content-Type",
          "value": "application/json"
        }
      ]
    }
  ]
}
```

This ensures your JSON is served with the correct CORS and content headers.

### Step 3: Deploy to Vercel

From the root of your folder, run:

```bash
vercel --prod
```

and answer the prompts to deploy your project.

When it's done, your file will be live at something like:

```
https://your-project-name.vercel.app/metric-card.json
```

### Step 4: Install the Component

Anyone can now run:

```bash
npx shadcn@latest add https://your-project-name.vercel.app/metric-card.json
```

No npm package, no build step, no complexity.


---
title: State
description: How to manage state in a component, as well as merging controllable and uncontrolled state.
type: reference
summary: Controlled vs uncontrolled state patterns and merging both modes with useControllableState from Radix UI.
prerequisites:
  - /composition
  - /types
related:
  - /as-child
---

# State



Building flexible components that work in both controlled and uncontrolled modes is a hallmark of professional components.

## Uncontrolled State

Uncontrolled state is when the component manages its own state internally. This is the default usage pattern for most components.

For example, here's a simple `Stepper` component that manages its own state internally:

```tsx title="stepper.tsx"
import { useState } from "react";

export const Stepper = () => {
  const [value, setValue] = useState(0);

  return (
    <div>
      <p>{value}</p>
      <button onClick={() => setValue(value + 1)}>Increment</button>
    </div>
  );
};
```

## Controlled State

Controlled state is when the component's state is managed by the parent component. Rather than keeping track of the state internally, we delegate this responsibility to the parent component.

Let's rework the `Stepper` component to be controlled by the parent component:

```tsx title="stepper.tsx"
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>
);
```

## Merging states

The best components support both controlled and uncontrolled state. This allows the component to be used in a variety of scenarios, and to be easily customized.

[Radix UI](https://www.radix-ui.com/) maintain an internal utility for merging controllable and uncontrolled state called [`use-controllable-state`](https://github.com/radix-ui/primitives/tree/main/packages/react/use-controllable-state). While not intended for public use, registries like [Kibo UI](https://www.kibo-ui.com) have implemented this utility to build their own Radix-like components.

Let's install the hook:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @radix-ui/react-use-controllable-state
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @radix-ui/react-use-controllable-state
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @radix-ui/react-use-controllable-state
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @radix-ui/react-use-controllable-state
    ```
  </CodeBlockTab>
</CodeBlockTabs>

This lightweight hook gives you the same state management patterns used internally by Radix UI's component library, ensuring your components behave consistently with industry standards.

The hook accepts three main parameters and returns a tuple with the current value and setter. Let's use it to merge the controlled and uncontrolled state of the `Stepper` component:

```tsx title="stepper.tsx"
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, // The controlled value prop
    defaultProp: defaultValue, // Default value for uncontrolled mode
    onChange: onValueChange, // Called when value changes
  });

  return (
    <div>
      <p>{value}</p>
      <button onClick={() => setValue(value + 1)}>Increment</button>
    </div>
  );
};
```


---
title: Styling
description: Conditional and composable styling with Tailwind classes.
type: guide
summary: Using tailwind-merge, clsx, the cn utility, and Class Variance Authority for conditional and composable component styling.
prerequisites:
  - /composition
related:
  - /design-tokens
  - /data-attributes
---

# Styling



Modern component libraries need flexible styling systems that can handle complex requirements without sacrificing developer experience. The combination of Tailwind CSS with intelligent class merging has emerged as a powerful pattern for building customizable components.

This approach solves the fundamental tension between providing sensible defaults and allowing complete customization - a challenge that has plagued component libraries for years.

## The problem with traditional styling

Traditional CSS approaches often lead to specificity wars, style conflicts, and unpredictable overrides. When you pass `className="bg-blue-500"` to a component that already has `bg-red-500`, which one wins?

Without proper handling, both classes apply and the result depends on a lot of factors - CSS source order, the specificity of the classes, the bundler's class merging algorithm, etc.

## Merging classes intelligently

The `tailwind-merge` library solves this by understanding Tailwind's class structure and intelligently resolving conflicts. When two classes target the same CSS property, it keeps only the last one.

```tsx title="Without tailwind-merge"
// Both bg-red-500 and bg-blue-500 apply - unpredictable result
<Button className="bg-blue-500" />
// Renders: className="bg-red-500 bg-blue-500"
```

```tsx title="With tailwind-merge"
import { twMerge } from "tailwind-merge";

// bg-blue-500 wins as it comes last
const className = twMerge("bg-red-500", "bg-blue-500");
// Returns: "bg-blue-500"
```

This works for all Tailwind utilities:

```tsx
twMerge("px-4 py-2", "px-8"); // Returns: "py-2 px-8"
twMerge("text-sm", "text-lg"); // Returns: "text-lg"
twMerge("hover:bg-red-500", "hover:bg-blue-500"); // Returns: "hover:bg-blue-500"
```

The library understands Tailwind's modifier system too:

```tsx
// Modifiers are handled correctly
twMerge("hover:bg-red-500 focus:bg-red-500", "hover:bg-blue-500");
// Returns: "focus:bg-red-500 hover:bg-blue-500"
```

## Conditional classes

Often you need to apply classes conditionally based on props or state. The `clsx` library provides a clean API for this:

```tsx title="Using clsx"
import clsx from "clsx";

// Basic conditionals
clsx("base", isActive && "active");
// Returns: "base active" (if isActive is true)

// Object syntax
clsx("base", {
  active: isActive,
  disabled: isDisabled,
});

// Arrays
clsx(["base", isLarge ? "text-lg" : "text-sm"]);

// Mixed
clsx(
  "base",
  ["array-item"],
  { "object-conditional": true },
  isActive && "conditional",
);
```

A common pattern is to merge a default set of classes with incoming props, as well as any custom logic we have:

```tsx title="component.tsx"
const Component = ({ className, ...props }: ComponentProps) => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div
      className={cn(
        "rounded-lg border bg-white shadow-sm",
        isOpen && "bg-blue-500",
        className,
      )}
      {...props}
    />
  );
};
```

## The `cn` utility function

The `cn` function, popularized by [shadcn/ui](https://ui.shadcn.com/), combines `clsx` and `tailwind-merge` to give you both conditional logic and intelligent merging:

```tsx title="lib/utils.ts"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
```

The power comes from the ordering - base styles first, conditionals second, user overrides last. This ensures predictable behavior while maintaining full customization.

## Class Variance Authority (CVA)

For complex components with many variants, manually managing conditional classes becomes unwieldy. [Class Variance Authority (CVA)](https://cva.style/docs) provides a declarative API for defining component variants.

For example, here's an extract from the [Button](https://ui.shadcn.com/docs/components/button) component from shadcn/ui:

```tsx title="@/components/ui/button.tsx"
const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive:
          "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
        outline:
          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost:
          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2 has-[>svg]:px-3",
        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
        icon: "size-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  },
);
```

## Best practices

### 1. Order matters

Always 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)

```tsx
className={cn(
  'base-styles',            // 1. Base
  variant && variantStyles, // 2. Variants
  isActive && 'active',     // 3. Conditionals
  className                 // 4. User overrides
)}
```

### 2. Document your variants

Use TypeScript and JSDoc to document what each variant does:

```tsx
type ButtonProps = {
  /**
   * The visual style of the button
   * @default "primary"
   */
  variant?: "primary" | "secondary" | "destructive" | "ghost";

  /**
   * The size of the button
   * @default "md"
   */
  size?: "sm" | "md" | "lg";
};
```

### 3. Extract repeated patterns

If you find yourself writing the same conditional logic repeatedly, extract it:

```tsx title="utils/styles.ts"
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';

// Use in components
className={cn(focusRing, disabled, className)}
```

## Migration guide

If you're migrating from a different styling approach, here's how to adapt common patterns:

### From CSS Modules

```tsx title="Before - CSS Modules"
import styles from "./Button.module.css";

<button className={`${styles.button} ${styles[variant]} ${className}`} />;
```

```tsx title="After - cn + Tailwind"
import { cn } from "@/lib/utils";

<button
  className={cn(
    "px-4 py-2 rounded-lg",
    variant === "primary" && "bg-blue-500 text-white",
    className,
  )}
/>;
```

### From styled-components

```tsx title="Before - styled-components"
const Button = styled.button<{ $primary?: boolean }>`
  padding: 8px 16px;
  background: ${(props) => (props.$primary ? "blue" : "gray")};
`;
```

```tsx title="After - cn + Tailwind"
function Button({ primary, className, ...props }) {
  return (
    <button
      className={cn(
        "px-4 py-2",
        primary ? "bg-blue-500" : "bg-gray-500",
        className,
      )}
      {...props}
    />
  );
}
```

## Performance considerations

Both `clsx` and `tailwind-merge` are highly optimized, but keep these tips in mind:

1. **Define variants outside components** - CVA variants should be defined outside the component to avoid recreation on every render.

2. **Memoize complex computations** - If you have expensive conditional logic, consider memoizing:

```tsx
const className = useMemo(
  () => cn(baseStyles, expensiveComputation(props), className),
  [props, className],
);
```

3. **Use CSS variables for dynamic values** - Instead of generating classes dynamically, use CSS variables:

```tsx title="Prefer CSS variables"
// Good
<div
  className="bg-[var(--color)]"
  style={{ '--color': dynamicColor } as React.CSSProperties}
/>

// Avoid
<div className={`bg-[${dynamicColor}]`} />
```

The combination of Tailwind CSS, intelligent class merging, and variant APIs provides a robust foundation for component styling. This approach scales from simple buttons to complex design systems while maintaining predictability and developer experience.


---
title: Types
description: Extending the browser's native HTML elements for maximum customization.
type: reference
summary: Patterns for extending native HTML attributes, single-element wrapping, exporting prop types, and avoiding prop name conflicts.
prerequisites:
  - /composition
related:
  - /as-child
  - /polymorphism
  - /state
---

# Types



When building reusable components, proper typing is essential for creating flexible, customizable, and type-safe interfaces. By following established patterns for component types, you can ensure your components are both powerful and easy to use.

## Single Element Wrapping

Each exported component should ideally wrap a single HTML or JSX element. This principle is fundamental to creating composable, customizable components.

When a component wraps multiple elements, it becomes difficult to customize specific parts without prop drilling or complex APIs. Consider this anti-pattern:

```tsx title="@/components/ui/card.tsx"
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>
);
```

As we discussed in [Composition](/composition), this approach creates several problems:

* You can't customize the header styling without adding more props
* You can't control the HTML elements used for title and description
* You're forced into a specific DOM structure

Instead, each layer should be its own component. This allows you to customize each layer independently, and to control the exact HTML elements used for the title and description.

The benefits of this approach are:

* **Maximum customization** - Users can style and modify each layer independently
* **No prop drilling** - Props go directly to the element that needs them
* **Semantic HTML** - Users can see and control the exact DOM structure
* **Better accessibility** - Direct control over ARIA attributes and semantic elements
* **Simpler mental model** - One component = one element

## Extending HTML Attributes

Every component should extend the native HTML attributes of the element it wraps. This ensures users have full control over the underlying HTML element.

### Basic Pattern

```tsx
export type CardRootProps = React.ComponentProps<"div"> & {
  // Add your custom props here
  variant?: "default" | "outlined";
};

export const CardRoot = ({ variant = "default", ...props }: CardRootProps) => (
  <div {...props} />
);
```

### Common HTML Attribute Types

React provides type definitions for all HTML elements. Use the appropriate one for your component:

```tsx
// For div elements
type DivProps = React.ComponentProps<"div">;

// For button elements
type ButtonProps = React.ComponentProps<"button">;

// For input elements
type InputProps = React.ComponentProps<"input">;

// For form elements
type FormProps = React.ComponentProps<"form">;

// For anchor elements
type LinkProps = React.ComponentProps<"a">;
```

### Handling Different Element Types

When a component can render as different elements, use generics or union types:

```tsx
// Using discriminated unions
export type ButtonProps =
  | (React.ComponentProps<"button"> & { asChild?: false })
  | (React.ComponentProps<"div"> & { asChild: true });

// Or with a polymorphic approach
export type PolymorphicProps<T extends React.ElementType> = {
  as?: T;
} & React.ComponentPropsWithoutRef<T>;
```

### Extending custom components

If you're extending an existing component, you can use the `ComponentProps` type to get the props of the component.

```tsx title="@/components/ui/share-button.tsx"
import type { ComponentProps } from "react";

export type ShareButtonProps = ComponentProps<"button">;

export const ShareButton = (props: ShareButtonProps) => <button {...props} />;
```

## Exporting Types

Always export your component prop types. This makes them accessible to consumers for various use cases.

Exporting types enables several important patterns:

```tsx
// 1. Extracting specific prop types
import type { CardRootProps } from "@/components/ui/card";
type variant = CardRootProps["variant"];

// 2. Extending components
export type ExtendedCardProps = CardRootProps & {
  isLoading?: boolean;
};

// 3. Creating wrapper components
const MyCard = (props: CardRootProps) => (
  <CardRoot {...props} className={cn("my-custom-class", props.className)} />
);

// 4. Type-safe prop forwarding
function useCardProps(): Partial<CardRootProps> {
  return {
    variant: "outlined",
    className: "custom-card",
  };
}
```

Your exported types should be named `<ComponentName>Props`. This is a convention that helps other developers understand the purpose of the type.

## Best Practices

### 1. Always Spread Props Last

Ensure users can override any default props:

```tsx
// ✅ Good - user props override defaults
<div className="default-class" {...props} />

// ❌ Bad - defaults override user props
<div {...props} className="default-class" />
```

### 2. Avoid Prop Name Conflicts

Don't use prop names that conflict with HTML attributes unless intentionally overriding:

```tsx
// ❌ Bad - conflicts with HTML title attribute
export type CardProps = React.ComponentProps<"div"> & {
  title: string; // This conflicts with the HTML title attribute
};

// ✅ Good - use a different name
export type CardProps = React.ComponentProps<"div"> & {
  heading: string;
};
```

### 3. Document Custom Props

Add JSDoc comments to custom props for better developer experience:

```tsx
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;
};
```
