Skip to main content

Advanced: Applying to Other Components

What you learn in this chapter

You apply the patterns learned with Button to other components (Badge, Input, Card, and Alert). By reusing the same patterns and design tokens, consistency emerges across components.

A recap of the patterns you learned

The core patterns
1. cva … styles per variant
2. cn … safe class merging (+ ref pass-through with ...props)
3. Slot … element substitution (asChild)
4. Design tokens … consistent references for color and spacing

Every component can be built with this combination. All of them use the React 19 style (no forwardRef).

Example 1: Badge (cva + cn)

A Badge used for status display and the like. It has variants, so use cva; for colors, use tokens.

// src/components/ui/badge.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground",
secondary: "border-transparent bg-secondary text-secondary-foreground",
destructive: "border-transparent bg-destructive text-white",
outline: "text-foreground",
},
},
defaultVariants: { variant: "default" },
}
)

function Badge({
className,
variant,
...props
}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
return <span data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />
}

export { Badge, badgeVariants }
<Badge>Default</Badge>
<Badge variant="secondary">Draft</Badge>
<Badge variant="destructive">Error</Badge>
<Badge variant="outline">Outline</Badge>

You can make layout fine-tunings, such as spacing and placement, with className (cn merges them safely).

{/* a notification-count badge: place it next to a heading with some spacing */}
<h2>
Notifications<Badge variant="destructive" className="ml-2">3</Badge>
</h2>

{/* top spacing when lining it up with an icon or text */}
<Badge variant="secondary" className="mt-1">Draft</Badge>

Adding an icon makes the status clear at a glance. This book's Badge has no gap in its base, so add the spacing between the icon and text with gap-1 in className (this is also a layout adjustment, so it is OK).

import { Check, Clock } from "lucide-react"

<Badge className="gap-1">
<Check className="size-3" />
Done
</Badge>
<Badge variant="secondary" className="gap-1">
<Clock className="size-3" />
In progress
</Badge>
<Badge variant="destructive" className="gap-1">
<Clock className="size-3" />
Overdue
</Badge>
info

Badge is a simple case that does not need asChild. When you need it, you can add it just like in Button.

tip

If you frequently use it with an icon, instead of writing className="gap-1" every time, you can add gap-1 to the base class of badgeVariants. When you "push frequently used combinations into the variant side", the consuming code becomes cleaner.

tip

When you want to change the color or look, do not override it with className; add a cva variant. For example, when you want "success (green)", instead of className="bg-green-500", define a success variant on variant (the hands-on practice chapter, Exercise 1). On the other hand, layout adjustments like mt-2 or ml-1 are fine with className (cn merges them safely; the cn chapter). The trick to not breaking the design system is to keep colors consistent with variants and adjust only placement locally.

Example 2: Input (cn only)

An input field. It has few visual variations, so do not use cva; build it with cn only.

The key point is passing ref through with ...props. Input fields are often used with form libraries like React Hook Form, which operate the input's DOM directly through ref. For example, processes like "automatically focus the input field with a validation error" or "read the entered value". If Input does not pass ref through, it cannot integrate with such libraries. In React 19, ref is also included in ...props (the ref chapter), so this works with no special handling.

// src/components/ui/input.tsx
import * as React from "react"

import { cn } from "@/lib/utils"

function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
)
}

export { Input }
<Input placeholder="Your name" />
<Input type="email" placeholder="Email address" />
<Input disabled placeholder="Disabled" />

Example 3: Card (a compound component)

Card is a "compound component" pattern that combines several small components. Build each with the same shape (ComponentProps + cn + data-slot).

data-slot shines here. From the parent element, you can target children to apply styles, like [&_[data-slot=card-header]]:border-b, which is handy for styling a compound component all at once.

// src/components/ui/card.tsx
import * as React from "react"

import { cn } from "@/lib/utils"

function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
{...props}
/>
)
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-header" className={cn("flex flex-col gap-1.5 p-6", className)} {...props} />
}

function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-title" className={cn("font-semibold leading-none", className)} {...props} />
}

function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-content" className={cn("p-6 pt-0", className)} {...props} />
}

// CardDescription / CardFooter can be built the same way

export { Card, CardHeader, CardTitle, CardContent }
<Card>
<CardHeader>
<CardTitle>Card title</CardTitle>
</CardHeader>
<CardContent>
<p>This is the card content.</p>
<Button>Action</Button>
</CardContent>
</Card>

Example 4: Alert (cva + cn + asChild)

An example that uses the same 4-piece set as Button. With asChild, you can make it a semantic element such as <section>.

// src/components/ui/alert.tsx
import * as React from "react"
import { Slot } from "radix-ui"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

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

function Alert({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "div"
return (
<Comp
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}

export { Alert, alertVariants }
<Alert variant="destructive">
<strong>Error:</strong> Something went wrong.
</Alert>

<Alert asChild>
<section aria-label="Success message">
<strong>Success:</strong> Your changes have been saved.
</section>
</Alert>

Pattern selection guide

Q. Are there style variations? → use cva
Q. Do you want to allow customization with className? → use cn (+ ...props)
Q. Do you want to swap the rendered element? → use Slot (asChild)

※ In React 19, ref is passed through within ...props, so no special handling (forwardRef) is needed.
ComponentcvacnasChild
Button
Badge-
Input--
Card--
Alert

For every component, cn and ...props are common. You add cva when there are variants, and asChild when you want to swap the element.

Best practices for component design

1. Single responsibility

Keep each component to a single responsibility and combine them.

// ✅ separate responsibilities and compose
<Tooltip content="Submit">
<Button>
<CheckIcon />
Submit
</Button>
</Tooltip>

2. Provide default values

With defaultVariants, make it usable even without props.

3. Pull in HTML attributes with ComponentProps

// use the ComponentProps that matches each element
React.ComponentProps<"button"> // button
React.ComponentProps<"input"> // input field
React.ComponentProps<"div"> // a general-purpose container

4. Specify colors with design tokens

By using tokens like bg-primary / bg-destructive instead of bg-blue-500, the colors and theme of all components align. This is the essence of consistency.

The diff from the real Button

Let's take a look at the "real Button" that the final-form chapter hinted at. The current shadcn/ui registry's Button keeps the same structure as the one built in this book (cva + cn + Slot + tokens) and is mainly extended in the following 3 points.

  1. 8 sizes: in addition to this book's default / sm / lg / icon, it defines xs / icon-xs / icon-sm / icon-lg.
  2. data-variant / data-size attributes: in addition to data-slot="button", it also outputs data-variant and data-size, which indicate the variant in effect. You can target buttons "per variant / size" from CSS or tests.
  3. Classes such as automatic icon adjustment: it has even more utilities, such as [&_svg]-family classes that automatically adjust the size and spacing of child SVG icons, and classes that handle the look when aria-invalid.

In all cases, "more classes and variants were added"; the way you read it is no different from this book's Button.

Reading other shadcn/ui components

With the knowledge from this book, you become able to read other shadcn/ui components too.

Simple: Badge, Input, Label → the basics of cva + cn
Intermediate: Card, Alert, Avatar → compound components
Advanced: Dialog, DropdownMenu → integration with Radix UI
Expert: Form, DataTable → integration with external libraries

Chapter summary

  • The patterns you learned (cva / cn / Slot / tokens) can be applied to any component
  • For all of them, cn + ...props is the common foundation
  • Add cva for variants and asChild for element substitution
  • By using common design tokens, consistency emerges across components

In the next chapter, you learn the perspective of bringing the individual components you have built into a single "design system".