# How to Understand the shadcn Style System and Create Custom Components

> Master the shadcn style system with Tailwind CSS, cva, and cn. Learn to build custom, themeable components efficiently directly in your codebase. Unlock elegant UI development.

- Repository: [shadcn-ui/ui](https://github.com/shadcn-ui/ui)
- Tags: deep-dive
- Published: 2026-02-26

---

**The shadcn style system combines Tailwind CSS with two utility functions—`cva` (class-variance-authority) for type-safe variant definitions and `cn` for intelligent class merging—to let you build custom, themeable components without leaving your codebase.**

The `shadcn-ui/ui` repository ships a Tailwind-first component library that centralizes design tokens while keeping every component editable in your own project. Understanding how the shadcn style system orchestrates `cva`, `cn`, and Tailwind’s configuration allows you to extend existing patterns or craft entirely bespoke UI elements.

## Core Architecture of the shadcn Style System

### Tailwind Configuration and Content Scanning

Tailwind’s configuration lives at the project root, but the shadcn style system extends it via registry-specific configs to ensure all component files are scanned for class extraction. In `deprecated/www/tailwind.config.cjs`, the registry folder is added to the `content` array:

```javascript
// deprecated/www/tailwind.config.cjs
const baseConfig = require("../../tailwind.config.cjs")

module.exports = {
  ...baseConfig,
  content: [
    ...baseConfig.content,
    "content/**/*.mdx",
    "registry/**/*.{ts,tsx}",
  ],
}

```

This setup means any custom component you place under `apps/v4/registry/…` or `registry/…` is automatically picked up by Tailwind’s JIT engine without additional configuration changes.

### The `cn` Utility: Class Merging with `twMerge`

The `cn` function is a thin wrapper around **clsx** (for conditional class concatenation) and **tailwind-merge** (for deduplication and conflict resolution). Defined in [`apps/v4/lib/utils.ts`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/lib/utils.ts), it ensures deterministic class output:

```typescript
// apps/v4/lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

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

```

Every shadcn component uses `cn` to merge the result of a `cva` call with user-provided `className` props, preventing duplicate or conflicting Tailwind utilities.

### Variant Definitions with `cva`

Components encode visual variants using `cva` (class-variance-authority). The `cva` call returns a function that, given a variant object, produces a string of Tailwind classes. The `Button` component in [`apps/v4/registry/new-york-v4/ui/button.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/button.tsx) demonstrates the full pattern:

```tsx
// apps/v4/registry/new-york-v4/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Slot } from "radix-ui"

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-white hover:bg-destructive/90",
        outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
        secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        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",
    },
  }
)

function Button({ className, variant = "default", size = "default", asChild = false, ...props }) {
  const Comp = asChild ? Slot.Root : "button"
  return (
    <Comp
      data-slot="button"
      data-variant={variant}
      data-size={size}
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { Button, buttonVariants }

```

Key architectural points:

- **Base string** contains always-present utilities (layout, focus states, disabled states).
- **`variants`** map each variant name to its specific Tailwind classes.
- **`defaultVariants`** provide fallback values when consumers omit props.
- **`cn(buttonVariants({ … }))`** merges generated classes with any extra `className` passed by the caller.

## Creating Custom Styles in the shadcn Style System

### Adding a New Component File

Place your component under the registry (e.g., [`apps/v4/registry/custom/ui/alert.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/custom/ui/alert.tsx)). This folder structure mirrors existing components like those in `new-york-v4`, ensuring Tailwind automatically tracks your new files.

```tsx
// apps/v4/registry/custom/ui/alert.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { X } from "lucide-react"

const alertVariants = cva(
  "relative w-full rounded-md border p-4",
  {
    variants: {
      variant: {
        default: "bg-background text-foreground border-muted",
        destructive: "bg-destructive text-destructive-foreground border-destructive/50",
      },
    },
    defaultVariants: { variant: "default" },
  }
)

interface AlertProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof alertVariants> {
  /** Optional icon component */
  icon?: React.ReactNode
}

export function Alert({ className, variant = "default", icon, children, ...props }: AlertProps) {
  return (
    <div className={cn(alertVariants({ variant, className }))} {...props}>
      {icon && <span className="mr-2">{icon}</span>}
      {children}
    </div>
  )
}

```

Compared to the [`button.tsx`](https://github.com/shadcn-ui/ui/blob/main/button.tsx) implementation, this example defines only a `variant` dimension (no `size`), uses a simpler base class string appropriate for alerts, and accepts an optional `icon` prop for flexibility.

### Consuming Your Custom Component

Import and use your component exactly like any other shadcn component:

```tsx
import { Alert } from "@/registry/custom/ui/alert"
import { X } from "lucide-react"

export function Demo() {
  return (
    <Alert variant="destructive" icon={<X className="h-4 w-4" />}>
      Something went terribly wrong.
    </Alert>
  )
}

```

Because the component uses `cn` internally, any additional `className` you pass merges cleanly without duplicate utilities:

```tsx
<Alert className="shadow-lg" />

```

### Extending Existing Variant Sets

When you need a new size or variant for an existing component (like `Button`), re-export the original `cva` instance and append your custom variants:

```tsx
// apps/v4/registry/custom/ui/button-extended.tsx
import { buttonVariants as baseButtonVariants } from "@/registry/new-york-v4/ui/button"
import { cva } from "class-variance-authority"

const buttonVariants = cva(baseButtonVariants, {
  variants: {
    size: {
      ...baseButtonVariants?.options?.variants?.size,
      xl: "h-12 px-8 text-lg",
    },
  },
})

export { buttonVariants }

```

This approach preserves the original variant definitions while adding an `xl` size option. Any component that imports `buttonVariants` from this extended file will recognize the new size without duplicating the entire variant map.

## Practical Tips for the shadcn Style System

| Situation | Recommended Approach |
|-----------|----------------------|
| **Add a brand-specific color** | Extend Tailwind’s color palette in `tailwind.config.cjs`, then reference the new token inside a `cva` variant (e.g., `bg-brand-primary`). |
| **Create a shared layout component** | Keep it in `registry/shared/ui/…` so every app can import it via the alias `@/registry/shared/ui/...`. |
| **Override a component for a single project** | Duplicate the component file in your app’s `src/components/…`, import the original `cva` for consistency, and adjust only the variant map you need. |
| **Debug generated class strings** | Log `buttonVariants({ variant, size })` in the component; the output will be a plain string you can copy-paste into browser dev-tools to verify the Tailwind utilities. |

## Key Files to Explore in the shadcn-ui Repository

| File | Role | Link |
|------|------|------|
| [`apps/v4/lib/utils.ts`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/lib/utils.ts) – `cn` helper | Merges class strings safely | <https://github.com/shadcn-ui/ui/blob/main/apps/v4/lib/utils.ts> |
| [`apps/v4/registry/new-york-v4/ui/button.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/button.tsx) – Button implementation | Shows a full `cva` + `cn` pattern | <https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/button.tsx> |
| [`templates/monorepo-next/packages/ui/src/lib/utils.ts`](https://github.com/shadcn-ui/ui/blob/main/templates/monorepo-next/packages/ui/src/lib/utils.ts) – Alternative `cn` definition (used in templates) | Same utility in a different workspace | <https://github.com/shadcn-ui/ui/blob/main/templates/monorepo-next/packages/ui/src/lib/utils.ts> |
| `deprecated/www/tailwind.config.cjs` – Registry Tailwind config | Demonstrates how the registry is added to Tailwind’s `content` | <https://github.com/shadcn-ui/ui/blob/main/deprecated/www/tailwind.config.cjs> |
| [`packages/shadcn/src/utils/templates.ts`](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/src/utils/templates.ts) – Component scaffolding logic | Generates component files for the CLI, helpful to see the intended structure | <https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/src/utils/templates.ts> |
| [`apps/v4/registry/new-york-v4/lib/utils.ts`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/lib/utils.ts) – Same `cn` in the V4 app | Shows the alias resolution (`@/lib/utils`) | <https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/lib/utils.ts> |

## Summary

- **Tailwind CSS** provides the design tokens and scans the `registry` folder for utility classes.
- **`cva`** (class-variance-authority) encapsulates component-level variants in a type-safe map, separating base styles from variant-specific overrides.
- **`cn`** combines `clsx` and `tailwind-merge` to merge generated classes with user-supplied `className` props while eliminating duplicates and resolving Tailwind conflicts.
- To **create custom styles**, add a new component file under the registry, define a `cva` map for your variants, and export a component that uses `cn` to merge classes.
- **Extend existing components** by re-importing their base `cva` instances and appending new variants, preserving the original design system while adding bespoke options.

## Frequently Asked Questions

### What is the difference between `cva` and `cn` in the shadcn style system?

`cva` (class-variance-authority) is responsible for defining and generating variant-based class strings according to a schema you define (e.g., mapping `variant="destructive"` to specific background and text colors). `cn` is a runtime utility that merges arbitrary class strings—including the output of `cva`—while removing duplicates and resolving Tailwind CSS conflicts using `tailwind-merge`. You typically call `cn(buttonVariants({ variant }), className)` to combine generated variants with user overrides.

### How do I add a new color token to the shadcn style system?

Extend your `tailwind.config.cjs` (or [`tailwind.config.ts`](https://github.com/shadcn-ui/ui/blob/main/tailwind.config.ts)) to include the new color in the theme extension, then reference it inside your `cva` variant definitions. For example, add `brand: { primary: "#3b82f6" }` to `theme.extend.colors`, then use `bg-brand-primary` or `text-brand-primary` within your component’s `cva` map. Because the registry folder is included in Tailwind’s `content` array, any new utilities will be automatically generated.

### Can I override shadcn component styles without forking the repository?

Yes. Copy the component file (e.g., [`button.tsx`](https://github.com/shadcn-ui/ui/blob/main/button.tsx)) from the registry into your application’s local component directory (e.g., [`src/components/ui/button.tsx`](https://github.com/shadcn-ui/ui/blob/main/src/components/ui/button.tsx)), then modify the `cva` variant map or the component logic directly. Import the original `cva` definition if you only need to add variants rather than replace them. Because shadcn components are installed directly into your codebase—not consumed as a npm package—you maintain full control over styling without maintaining a fork.

### Where should I place custom components in a shadcn project?

Place custom components under the registry path used by your application, typically `apps/v4/registry/[theme]/ui/` for the v4 application or `registry/` in older structures. This ensures Tailwind’s JIT engine scans your files for utility classes. Use path aliases (e.g., `@/registry/custom/ui/alert`) to import components consistently across your application. If you are working in a monorepo, consider a shared registry folder (e.g., `registry/shared/ui/`) that multiple applications can reference.