DesignAPI dei componenti: 6 regole sulle prop che evitano gli errori
Quattro prop booleane danno 16 versioni dello stesso bottone. Sei regole per API dei componenti in cui l'uso sbagliato non compila, con il TypeScript.

Progettare l'API di un componente vuol dire scegliere le prop in modo che l'uso previsto sia il più breve da scrivere e quello sbagliato si fermi prima di arrivare a schermo. Nelle prop una decisione di design smette di essere un disegno e diventa una regola: un file Figma mostra che un bottone ha tre varianti, ma solo la firma del tipo può rifiutarne una quarta.
Chi gestisce un solo design system su più prodotti incontra il problema per primo. Ogni prop che accetta una combinazione mai disegnata è una combinazione che prima o poi qualcuno rilascia. Qui sotto trovi le sei regole che applichiamo alle prop, un esempio completo su un bottone e quello che un'API rigida costa.
Perché un'API dei componenti troppo permissiva costa tanto?
Prendiamo un bottone con quattro booleani opzionali: primary, secondary, danger e small. Quattro flag indipendenti danno 16 combinazioni. Il file di design ne mostra poche. Le altre non sono vietate da nessuna parte che il compilatore possa vedere, e quindi arrivano in produzione con normali pull request: un bottone primario e secondario insieme, che prende la classe vincente nella cascata, o un'azione distruttiva disegnata come un link discreto.
Nessun test le intercetta, perché nulla va in errore. Emergono come deriva: la stessa azione disegnata in tre modi su tre schermate. Abbiamo descritto come si accumula nell'articolo sul component drift. Quasi sempre entra dalle prop.
Perché la documentazione non basta?
La risposta classica è una pagina d'uso che dice "non combinare primary e secondary". La pagina ha ragione e cambia poco, perché chi sviluppa legge la firma del tipo nell'editor, non il sito della documentazione. Quando documentazione e tipo non coincidono, vince il tipo ogni volta che qualcuno preme l'autocompletamento. La regola va spostata nel tipo.
Regola 1: una sola prop di variante al posto di booleani in concorrenza
Se più prop descrivono aspetti che si escludono a vicenda, in realtà sono una prop sola con più valori. La guida all'API di MUI confronta le due forme ed elenca cosa porta l'enum: impedisce le combinazioni non valide, limita il numero di prop esposte e accoglie nuovi valori senza nuove prop. Il prezzo è qualche carattere: variant="primary" invece di primary.
La verifica richiede un secondo. Se due booleani attivi insieme non hanno senso visivo, non sono mai stati indipendenti. I booleani restano per gli stati che si combinano con tutto il resto, come disabled o loading.
Regola 2: le prop che dipendono l'una dall'altra diventano una discriminated union
Alcune prop hanno senso solo in coppia. Un bottone che porta a un'altra pagina vuole un href e nessun gestore del clic. Un bottone che esegue un'azione vuole un onClick e nessun href. Con due prop opzionali sono validi sia "tutte e due" sia "nessuna". Con una discriminated union di TypeScript ogni ramo dichiara cosa richiede e cosa vieta, e dentro il componente il compilatore restringe il tipo.
Yaron Minsky, di Jane Street, ha riassunto il principio in quattro parole: "make illegal states unrepresentable", cioè rendere impossibile scrivere gli stati non validi. Per le prop, gli stati non validi sono le combinazioni che altrimenti segnaleresti in un commento nel codice.
Regola 3: rendere obbligatorio ciò che l'accessibilità richiede
Un bottone con la sola icona non ha testo visibile, quindi non ha un nome accessibile finché chi lo usa non gliene dà uno. Primer, il design system di GitHub, mette questo compito in mano a chi chiama il componente: le note di accessibilità di IconButton chiedono di fornire un aria-label abbastanza descrittivo, che diventa il nome accessibile del bottone. Se la prop è obbligatoria nel tipo, un'etichetta mancante diventa un errore di compilazione sul computer di chi sviluppa, settimane prima che un audit la trovi. Vale anche per il testo alternativo di un'immagine, il titolo di una finestra di dialogo e l'etichetta di un campo.
Regola 4: le prop prendono il nome dal ruolo, mai dall'aspetto
color="red" descrive un pixel. tone="danger" descrive una decisione, e resta valido anche dopo il rebrand che rende il pericolo arancione. È la stessa distinzione che usiamo per i nomi dei design token: la prop parla il vocabolario semantico, i token dietro custodiscono i valori. E una stessa idea ha un solo nome in tutta la libreria. Se un componente accetta disabled, nessun altro deve accettare isEnabled.
Regola 5: chiudere la via di fuga per gli stili, o dichiararla come tale
Una prop style o className aperta su ogni componente è un secondo design system senza regole. Ogni override che passa da lì resta invisibile a chi mantiene il sistema finché non cambia qualcosa sotto. Due sistemi molto diffusi mostrano le due uscite possibili.
- React Spectrum di Adobe le dà un nome. Le sue vie di fuga si chiamano
UNSAFE_classNameeUNSAFE_style, e la guida agli stili sconsiglia con decisione di sovrascrivere gli stili di Spectrum, indicando di creare piuttosto un componente su misura. Ogni override si trova con una ricerca. - Primer di GitHub ne ha tolta una. Con Primer React v38, annunciato a ottobre 2025, la prop
sxnon è più supportata e gli stili passano daclassNamee CSS Modules. GitHub ha pubblicato anche un pacchetto temporaneo,@primer/styled-react, per i componenti che a valle usavano ancorasx: un'idea di quanto codice raccoglie in pochi anni una sola prop di stile lasciata aperta.
Qualunque strada scegli, conta gli usi. Se gli override crescono a ogni release, ti dicono quale variante manca al sistema.
Regola 6: comporre con i figli invece di aggiungere una prop per ogni caso
Le prop si moltiplicano quando un componente vuole gestire ogni layout: iconLeft, iconRight, badge, as, linkComponent. La composizione sposta quella variabilità nei figli. Con asChild di Radix una parte non disegna il suo elemento predefinito e passa prop e comportamento al figlio, così il trigger di un menu può diventare il link del router senza sapere che i router esistono. Il componente tiene le sue regole (focus, ARIA, tastiera) e cede solo l'elemento.
Come si traduce su un bottone vero?
Ecco un'API permissiva, del tipo che cresce una richiesta alla volta:
type ButtonProps = {
primary?: boolean
secondary?: boolean
danger?: boolean
small?: boolean
href?: string
onClick?: () => void
icon?: ReactNode
style?: CSSProperties
}Otto prop opzionali, ognuna valida insieme a tutte le altre. Lo stesso componente dopo le sei regole:
type ButtonBase = {
variant: 'primary' | 'secondary' | 'ghost'
tone?: 'neutral' | 'danger'
size?: 'small' | 'medium'
disabled?: boolean
children: ReactNode
}
type ButtonProps = ButtonBase & (
| { href: string; onClick?: never }
| { onClick: () => void; href?: never }
)
type IconButtonProps = Omit<ButtonBase, 'children'> & {
icon: IconName
label: string // becomes aria-label, and the tooltip
onClick: () => void
}Le varianti visive sono una prop sola. Il pericolo è un tono, quindi si combina con qualsiasi variante approvata dal design. Link e azione sono due rami, e passarli entrambi è un errore di tipo. Il bottone con la sola icona è un componente a parte, con l'etichetta obbligatoria. La prop style non c'è più: un aspetto che il sistema non prevede diventa la richiesta di una nuova variante, che è proprio la conversazione per cui il sistema esiste.
Quanto costa un'API rigida?
Tre cose, ed è meglio dirle prima che il team si impegni.
- I casi limite rallentano. Un team di prodotto che ha bisogno di un aspetto non previsto aspetta una nuova variante o scrive un componente suo. L'attesa è voluta, ma serve una strada breve: un processo di contribuzione che risponde in pochi giorni tiene i team dentro il sistema. Quando la risposta arriva dopo un trimestre, i team se ne vanno, ed è una delle cause che elenchiamo in perché l'adozione del design system si ferma.
- I tipi proteggono solo chi usa TypeScript. Chi consuma il componente in JavaScript puro non vede nessun errore di compilazione. Ai tipi vanno affiancati avvisi a runtime, attivi solo in sviluppo, per le combinazioni più pericolose.
- Togliere una prop è una breaking change. Restringere un'API esistente vuol dire una major e, su larga scala, un codemod. La migrazione alla v38 di Primer è un esempio pubblico di quanto lavoro comporta. Partire rigidi dalla prima release costa molto meno che diventarlo dopo.
Sul nostro sito un controllo nella build rifiuta gli stili inline, e ogni classe una tantum deve stare in un unico foglio di stile con prefisso, con una nota che indica il task che la porterà nel sistema. Il numero di eccezioni viene stampato a ogni esecuzione. È la regola 5 applicata a tutto il codice del sito: la via di fuga esiste, ha un nome e qualcuno la conta. Un'API più piccola è anche più facile da tenere piccola, ed è il ragionamento che facciamo nella dieta del design system.
Domande frequenti
Una sola className sull'elemento radice è un compromesso diffuso: chi usa il componente può posizionarlo nel layout (margini, posizione nella griglia) senza mettere mano alle sue parti interne. Quello che non dovrebbe consentire è ridisegnare l'interno, perché quegli override si rompono in silenzio quando il componente cambia. Primer ha tenuto className nella v38 togliendo sx, e React Spectrum tiene i suoi override sotto il prefisso UNSAFE_. Qualunque cosa consenti, fai in modo che si trovi con una ricerca, così puoi contarla.
Tutte e due permettono a un componente di diventare un elemento diverso. Una prop as cambia il tag, quindi i tipi del componente devono assorbire le prop di ogni elemento che potrebbe diventare, e in TypeScript la cosa si appesantisce in fretta. asChild, il pattern usato da Radix, passa il rendering al figlio, che conserva le sue prop tipizzate. Vanno bene entrambe se tutta la libreria usa la stessa; è il mescolarle che confonde chi le usa.
In tre release. Prima si aggiunge la nuova prop accanto alle vecchie e si mostra un avviso, solo in sviluppo, ogni volta che si usano quelle vecchie. Poi si passa un codemod sui progetti che usano il componente, finché gli avvisi arrivano a zero. Solo allora si tolgono le vecchie prop, in una major, perché con il versionamento semantico togliere una prop è una breaking change. Saltare la fase degli avvisi trasforma la major in una sorpresa per ogni team che aggiorna.
Servizi correlati
Articoli correlati
Design
DesignPerché rimuoviamo componenti invece di aggiungerli: la dieta del design system
La maggior parte dei design system cresce finché non si rompe. Trattiamo il conteggio dei componenti come un numero da tenere basso. Le regole che usiamo per potarlo ogni trimestre.
Design