Diego Betto
Shadcn/ui - how to install and use it (with examples)

Diego Betto · September 17, 2026 · 6 min di lettura

What is shadcn/ui and how to install and use it (with examples)

shadcn/ui isn't a component library like the others: you don't install it, you copy it. How the CLI works, what Radix UI and Tailwind do under the hood, and when it's actually worth using.

Condividi:XLinkedInFacebookWhatsApp

The first thing to understand about shadcn/ui is that it isn’t a library. There’s no shadcn-ui package to drop into package.json and bump with npm update. What you install is a CLI that copies the component’s source code — a readable, non-minified .tsx file, not something imported from node_modules — straight into your project. From that point on, the component is yours: you open it, read it, and edit it like any other file in the repo.

How the CLI works

Initial setup:

npx shadcn@latest init

This asks a few preference questions (style new-york or default, base color, whether to use CSS variables) and generates a components.json file that records those choices, plus sets up Tailwind and creates lib/utils.ts with a cn() helper — combining clsx and tailwind-merge to handle conditional class names without conflicts.

To add a component:

npx shadcn@latest add button

The CLI downloads the Button component’s code (and its dependencies, e.g. @radix-ui/react-slot) and writes it to src/components/ui/button.tsx. No version to track, no changelog to read before upgrading — because you’re not upgrading anything, you’re reading TSX code you just copied.

What’s underneath: Radix UI + Tailwind + CVA

Every shadcn/ui component is built on three pieces:

  • Radix UI Primitives: headless (unstyled) components that handle accessibility and interaction — focus trapping, keyboard handling, correct ARIA attributes on Dialog, Popover, DropdownMenu, and similar. It’s the hardest part to get right by hand, and shadcn/ui doesn’t reinvent it — it just reuses it.
  • Tailwind CSS: for visual styling. Each component is a handful of Tailwind classes applied to the Radix elements.
  • class-variance-authority (CVA): handles variants (variant="destructive", size="sm") in a typed way, generating the right class combinations instead of scattered if/else logic inside the component.

The result is a Button you technically wrote yourself, but that inherits Radix’s battle-tested accessibility and a Tailwind design system’s visual consistency.

Using it in practice

Once copied, Button is just a regular React component: import it and use its typed props.

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

export function Toolbar() {
  return (
    <div className="flex gap-2">
      <Button variant="default">Save</Button>
      <Button variant="destructive">Delete</Button>
      <Button variant="outline" size="sm">
        Cancel
      </Button>
    </div>
  );
}

More complex components follow the same pattern. Dialog, for example, composes several already-styled Radix parts (Trigger, Content, Header, Footer):

import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

export function DeleteAccountDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="destructive">Delete account</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you sure?</DialogTitle>
        </DialogHeader>
        <p className="text-sm text-muted-foreground">
          This action cannot be undone.
        </p>
      </DialogContent>
    </Dialog>
  );
}

There’s no “magic” API to learn beyond this: focus trapping, closing on Esc, correct aria-* attributes are already handled by Radix inside dialog.tsx, which you can open and read at any time.

The real advantage: you own the code

With a traditional component library (Material UI, Ant Design), customizing a style past a certain point means fighting the library’s API — override props, theme objects, !important. With shadcn/ui, the component is a file in your repo: if Dialog doesn’t do exactly what you need, you just edit it — no subclassing, no waiting for an upstream release.

That also makes it particularly convenient with AI-assisted code generation tools (v0, Cursor, Claude): the model sees normal TSX code in the project, not a black-box component imported from a package, so it can propose direct edits with much more context.

The trade-off

Owning the code also means maintaining it. If Radix UI ships an accessibility fix for Dialog, it doesn’t arrive via npm update — you have to re-run add dialog (which overwrites the file, so you lose any local changes unless you isolated them) or patch it by hand. For projects with dozens of customized components, that’s a real maintenance cost a traditional library doesn’t have.

It’s also worth noting that, despite the historical name “shadcn/ui”, the project was built specifically for React + Tailwind. Community-maintained ports exist for other frameworks (Vue, Svelte), but they don’t have the same coverage as the original — if your project isn’t React, check the port’s maturity before adopting it.

Pros and cons

Pros:

  • No dependency on a UI npm package: no version to keep in sync, no upstream breaking change hitting you by surprise.
  • Unlimited customization: you edit the file directly, there’s no override API to guess your way around.
  • Accessibility baseline already covered: inherits Radix UI’s work on focus, keyboard handling, and ARIA — redoing that from scratch is a huge amount of work.
  • Great fit for AI-assisted tooling: readable code in the repo, not a black-box package, so it’s easier for v0/Cursor/Claude to edit.
  • No library runtime overhead: no global provider, no library bundle to load — just the components you actually added.

Cons:

  • No automatic updates: an upstream fix from Radix or the component itself doesn’t arrive via npm update — you have to reapply it by hand or re-run add (risking overwriting your changes).
  • More code to maintain in the repo: every component you add is your code now, with the testing and maintenance burden that comes with it.
  • Tied to Tailwind: if your project doesn’t already use it, adopting shadcn/ui means adopting Tailwind too.
  • Uneven coverage outside React: Vue/Svelte ports exist but don’t match the original’s depth.
  • No semantic versioning: you can’t say “we’re on shadcn/ui 2.3” — every component has its own history of local edits.

FAQ

❓ Do I need to install shadcn/ui as a dependency?

No. The shadcn package (the CLI) is a one-off tool run via npx/pnpm dlx to generate or add components; the final code copied into the repo has no runtime dependency on any shadcn-ui package.

❓ Does shadcn/ui work without Tailwind?

Recent CLI versions support “plain” CSS variables without Tailwind for some setups, but the documented, intended experience is still Tailwind-based — outside that context, expect to manually adapt some of the classes.

❓ How do I update a component after it's changed upstream?

There’s no “sync” command: the safest option is manually diffing the updated file on the official registry against your local version and applying only the changes you need, or re-running add on a component you haven’t customized yet.

When it’s worth using

If your project is already on Tailwind and you want to start from accessible components without writing a WAI-ARIA-compliant Dialog or Combobox from scratch, shadcn/ui is probably the fastest option available today. If your team prefers a “closed-box” library with semantic versioning and centralized updates — or doesn’t use Tailwind and has no intention of adopting it — a traditional component library remains the simpler choice.

Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Written by

Diego Betto

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.