
Cos'è shadcn/ui, come installarlo e usarlo (con esempi)
shadcn/ui non è una libreria di componenti come le altre: non si installa, si copia. Come funziona la CLI, cosa fanno Radix UI e Tailwind sotto il cofano, e quando ha senso usarlo.
La prima cosa da capire su shadcn/ui è che non è una libreria. Non c’è un pacchetto shadcn-ui da metter in package.json e aggiornare con npm update. Quello che installi è una CLI che copia il codice sorgente dei componenti — un file .tsx leggibile, non minificato, non importato da node_modules — direttamente dentro il tuo progetto. Da quel momento il componente è tuo: lo apri, lo leggi, lo modifichi come qualsiasi altro file del repo.
Come funziona la CLI
Il setup iniziale:
npx shadcn@latest init
Questo comando chiede alcune preferenze (stile new-york o default, colore base, se usare CSS variables) e genera un file components.json che registra queste scelte, oltre a configurare Tailwind e creare lib/utils.ts con la funzione cn() — un helper che unisce clsx e tailwind-merge per gestire class name condizionali senza conflitti.
Per aggiungere un componente:
npx shadcn@latest add button
La CLI scarica il codice del componente Button (e le sue dipendenze, es. @radix-ui/react-slot) e lo scrive in src/components/ui/button.tsx. Nessuna versione da tracciare, nessun changelog da leggere prima di aggiornare — perché non stai aggiornando nulla, stai leggendo del codice TSX che hai appena copiato.
Cosa c’è sotto: Radix UI + Tailwind + CVA
Ogni componente shadcn/ui è costruito su tre pezzi:
- Radix UI Primitives: componenti headless (senza stile) che gestiscono accessibilità e interazione — focus trap, gestione tastiera, attributi ARIA corretti su
Dialog,Popover,DropdownMenue simili. È la parte più difficile da fare bene a mano, e shadcn/ui non la reinventa: la riusa. - Tailwind CSS: per lo stile visivo. Ogni componente è una manciata di classi Tailwind applicate agli elementi Radix.
class-variance-authority(CVA): gestisce le varianti (variant="destructive",size="sm") in modo tipizzato, generando le combinazioni di classi giuste invece di if/else sparsi nel componente.
Il risultato è un Button che tecnicamente hai scritto tu, ma che eredita l’accessibilità testata di Radix e la coerenza visiva di un design system Tailwind.
Usarlo in pratica
Una volta copiato, Button è un componente React come un altro: si importa e si usa con le sue prop tipizzate.
import { Button } from "@/components/ui/button";
export function Toolbar() {
return (
<div className="flex gap-2">
<Button variant="default">Salva</Button>
<Button variant="destructive">Elimina</Button>
<Button variant="outline" size="sm">
Annulla
</Button>
</div>
);
}
Componenti più complessi seguono lo stesso schema. Dialog, ad esempio, compone più parti Radix (Trigger, Content, Header, Footer) già stilizzate:
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">Elimina account</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Sei sicuro?</DialogTitle>
</DialogHeader>
<p className="text-sm text-muted-foreground">
L'operazione non è reversibile.
</p>
</DialogContent>
</Dialog>
);
}
Non c’è nessuna API “magica” da imparare oltre a questa: focus trap, chiusura con Esc, aria-* corretti sono già gestiti da Radix dentro dialog.tsx, che puoi aprire e leggere in qualsiasi momento.
Il vantaggio reale: possiedi il codice
Con una libreria di componenti tradizionale (Material UI, Ant Design), personalizzare uno stile oltre un certo punto significa combattere contro l’API della libreria — prop di override, temi, !important. Con shadcn/ui il componente è un file nel tuo repo: se il Dialog non fa esattamente quello che ti serve, lo modifichi e basta, senza subclassare nulla e senza aspettare una nuova release upstream.
Questo lo rende anche particolarmente comodo con strumenti di generazione codice assistiti da AI (v0, Cursor, Claude): il modello vede codice TSX normale nel progetto, non un componente black-box importato da un pacchetto, quindi può proporre modifiche dirette con più contesto.
Il rovescio della medaglia
Possedere il codice significa anche doverlo mantenere. Se Radix UI rilascia una fix di accessibilità sul Dialog, non arriva con un npm update — devi rieseguire add dialog (che sovrascrive il file, quindi perdi le tue modifiche se non le hai isolate) o applicare la patch a mano. Per progetti con decine di componenti personalizzati, questo è un costo di manutenzione reale che una libreria tradizionale non ha.
Vale anche la pena notare che, nonostante il nome storico “shadcn/ui”, il progetto è nato specificamente per React + Tailwind. Esistono porting mantenuti dalla community per altri framework (Vue, Svelte), ma non hanno lo stesso livello di copertura dell’originale — se il progetto non è React, verifica la maturità del porting prima di adottarlo.
Pro e contro
Pro:
- Zero dipendenza da un pacchetto npm della UI: nessuna versione da allineare, nessun breaking change upstream da subire a sorpresa.
- Personalizzazione senza limiti: modifichi il file direttamente, non c’è un’API di override da indovinare.
- Accessibilità di base già coperta: eredita il lavoro di Radix UI su focus, tastiera e ARIA, che rifare da zero è un lavoro enorme.
- Ottima integrazione con tool AI-assisted: codice leggibile nel repo, non un pacchetto black-box, quindi più facile da far modificare a v0/Cursor/Claude.
- Nessun overhead a runtime della libreria: non c’è un “provider” globale né un bundle di libreria da caricare, solo i componenti che hai effettivamente aggiunto.
Contro:
- Nessun aggiornamento automatico: una fix upstream di Radix o del componente stesso non arriva con
npm update, va riapplicata a mano o rieseguendoadd(con rischio di sovrascrivere le tue modifiche). - Più codice da mantenere nel repo: ogni componente aggiunto è codice tuo, con relativa responsabilità di test e manutenzione.
- Legato a Tailwind: se il progetto non lo usa già, adottare shadcn/ui significa adottare anche Tailwind.
- Coverage disomogenea fuori da React: i porting per Vue/Svelte esistono ma non hanno la stessa profondità dell’originale.
- Nessun versioning semantico: non puoi dire “sto usando shadcn/ui 2.3” — ogni componente ha la sua storia di modifiche locali.
FAQ
❓ Devo installare shadcn/ui come dipendenza?
No. Il pacchetto shadcn (CLI) si usa una tantum con npx/pnpm dlx per generare o aggiungere
componenti; il codice finale copiato nel repo non dipende da nessun pacchetto shadcn-ui a
runtime.
❓ shadcn/ui funziona senza Tailwind?
Le versioni più recenti della CLI supportano anche variabili CSS “plain” senza Tailwind per alcuni setup, ma l’esperienza pensata e documentata resta quella con Tailwind: fuori da quel contesto, aspettati di dover adattare manualmente parte delle classi.
❓ Come aggiorno un componente dopo che è stato modificato a monte?
Non c’è un comando di “sync”: l’opzione più sicura è confrontare manualmente il file aggiornato
sul registry ufficiale con la tua versione locale e applicare solo le modifiche che ti servono,
oppure rieseguire add su un componente non ancora personalizzato.
Quando ha senso usarlo
Se il progetto è già su Tailwind e vuoi partire da componenti accessibili senza scrivere da zero un Dialog o un Combobox conformi a WAI-ARIA, shadcn/ui è probabilmente la scelta più rapida disponibile oggi. Se invece il team preferisce una libreria “a scatola chiusa” con versioning semantico e aggiornamenti centralizzati — o non usa Tailwind e non ha intenzione di adottarlo — una libreria di componenti tradizionale resta la scelta più semplice.

Co-Fondatore & CTO presso PAPION. Senior full-stack engineer specializzato in React, TypeScript, Node.js e sicurezza applicativa.