Skip to main content

Take a look at the finished form: the big picture of the Button component

What you learn in this chapter

We preview the finished form of the Button component you build in this book. After grasping the big picture, we dig into each technique one at a time.

Why start from the finished form?

In learning to program, looking at the goal first is very effective.

Learning approach
- Common: A → B → C → D → finally complete
- This book: see the finished form → A → B → C → D → it clicks

By looking at the finished form first, you can see "where and why" each technique is used, which greatly improves learning efficiency.

The finished Button.tsx

Below is the Button component we will assemble in this book. This is the shadcn/ui Button, slightly tidied up for learning.

import * as React from "react"
import { Slot } from "radix-ui"
import { cva, type VariantProps } from "class-variance-authority"

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

const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-white hover:bg-destructive/90",
outline:
"border border-input bg-background 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",
sm: "h-8 px-3",
lg: "h-10 px-6",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)

function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"

return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}

export { Button, buttonVariants }
info

If this code makes you feel "this looks hard…", that is fine. In this book, we explain each of these elements carefully, in order. For now, it is enough to just look at it and think "so it's made of parts like these."

info

Colors like bg-primary and text-primary-foreground are not colors that come built into Tailwind; they are design tokens (meaningful colors decided for the project). We define these in the chapter on design tokens.

info

The reason export { Button, buttonVariants } at the end also exposes buttonVariants is for when you want to apply just the styles to another element without going through the Button component. For example, you can apply only the Button's appearance to a link with <a className={buttonVariants({ variant: "outline" })}>. It is not required if you only use the Button, but exposing it as a shadcn/ui convention is handy later.

Breaking down the code structure

This component is made of four major parts.

[The structure of Button.tsx]

├─ Part 1: Imports
│ ├─ React
│ ├─ Slot (from radix-ui)
│ ├─ cva, VariantProps (from class-variance-authority)
│ └─ cn (from @/lib/utils)

├─ Part 2: buttonVariants (style definition with cva)
│ ├─ base classes (shared styles)
│ ├─ variants (variant, size)
│ └─ defaultVariants (default values)

├─ Part 3: The type of Button's arguments
│ ├─ React.ComponentProps<"button"> (the button's HTML attributes)
│ ├─ VariantProps (the types of variant and size)
│ └─ asChild

└─ Part 4: The Button itself (a function component)
├─ receiving props
├─ choosing Comp (Slot or button)
└─ returning JSX

A list of the techniques in play

The techniques used in each part, and the chapter that teaches each in detail, are as follows.

TechniqueRoleChapter to learn more
Design tokens (bg-primary, etc.)Manage colors, spacing, etc. by meaningThe chapter on design tokens
cva / buttonVariantsDefine variants declarativelyThe cva chapter
cnCombine class names safelyThe cn chapter
ref (received as a prop)Pass a reference to a DOM elementThe ref chapter
Slot / asChildSwap the elementThe Slot chapter
data-slotA marker for styling and identificationGoing further
Types (ComponentProps, etc.)Type-safe propsThe TypeScript chapter
info

This component has no forwardRef. That is because in React 19, you can receive ref as a normal prop. Previously a mechanism called forwardRef was needed; we explain that history too in the ref chapter.

info

data-slot="button" is a marker showing that the element plays the "Button role (slot)." You use it to select with [data-slot="button"] in CSS, or to target child elements from the parent in a compound component (we practice this with the Card in Going further).

What is data-slot used for? (click to expand)

data-slot is a "styling hook" that shadcn/ui introduced into every component when it added Tailwind CSS v4 support. Its main uses are these three.

  1. Target children from the parent in composition (most important): without relying on class names, you can apply styles from the parent based on the child element's "role." For example, [data-slot="form"] [data-slot="button"] targets "all buttons inside a form," and for a Card, [&_[data-slot=card-header]]:border-b targets "the header inside a Card" (we practice this in Going further).
  2. Role-based selection (semantic targeting): you select elements by "purpose (role)" rather than by structure or class. You can also combine it with state attributes like [data-state="open"] to apply styles based on state.
  3. Unified state handling and identification in tests: it is useful when a compound component handles its children's states (such as focus) collectively, and for identifying elements in tests by a stable attribute rather than class names that tend to change.

The reason to use data-slot rather than class is that class can be overridden by the user via cn, whereas data-slot is an unchanging "role" label.

Basic usage

You use this Button as follows.

The default button

<Button>Click me</Button>

If you specify no props, the defaultVariants styles (variant: "default" / size: "default") are applied.

Specifying a variant

<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>

Specifying a size

<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon">🔍</Button>

Combining a variant and a size

<Button variant="destructive" size="lg">Delete account</Button>

Adding custom styles

<Button className="mt-4 w-full">Submit</Button>

Thanks to the cn function, the default styles and className are combined safely (the cn chapter).

The asChild pattern

<Button variant="outline" asChild>
<a href="/profile">View profile</a>
</Button>

The appearance stays that of a Button, but what actually renders is an <a> tag (the Slot chapter).

Using ref

import { useRef, useEffect } from "react"

function FocusButton() {
const buttonRef = useRef<HTMLButtonElement>(null)

useEffect(() => {
buttonRef.current?.focus()
}, [])

return <Button ref={buttonRef}>This is focused automatically</Button>
}

Passing a ref lets you access the button's DOM element. In React 19, this works without forwardRef (the ref chapter).

Why this design is excellent

This Button packs in the best practices of component design aligned with a design system.

1. Declarative style management

With cva, you can see at a glance which variant has which styles. It does not turn into a storm of if statements or ternary operators.

2. Safe customization

With the cn function, users can override styles safely via className. Even when they conflict with the defaults, the value passed later takes precedence correctly.

3. Preserving semantic HTML

With asChild, you can use the appropriate HTML element (such as <a> for a link) while keeping the appearance.

4. Type safety

Through integration with TypeScript, you get completion when using it, and passing an invalid value produces an error.

5. Accessibility

You can manage focus with ref, and consistent styles are applied to the focus-visible and disabled states as well.

6. Consistency with the design system

By using design tokens like bg-primary, colors and themes line up across the whole app. Change a single token, and it is reflected on every button (the chapter on design tokens).

Summary of this chapter

  • The finished form is made of four parts (imports / variant definition / types / the body)
  • The techniques in play are design tokens, cva, cn, ref, Slot, and types
  • In React 19, you can receive ref without forwardRef
  • This design embodies the design system principles of "declarative, safe, consistent"
info

The code shown here is tidied up for learning. The real shadcn/ui Button has even more classes and sizes, such as automatic icon adjustment and display on input errors. We cover the "real thing" in Going further.

In the next chapter, we first review the Tailwind CSS basics that form the foundation.