“La card dovrebbe avere un border-radius di 4px o 8px?”
Un thread durato 47 minuti. Quattro ingegneri, due designer, un file Figma con sei varianti. La discussione ha toccato le convenzioni iOS, le linee guida di Material Design, l’allineamento ottico e, a un certo punto, la sezione aurea. Nessuno aveva torto. Quello era il problema.
Ci eravamo già passati. Non solo con il border-radius — con le direzioni dei gradienti, i valori di blur delle ombre, i livelli di opacità per gli stati disabilitati. Ogni decisione era piccola. Ogni dibattito era ragionevole. E nel complesso, stavano divorando la nostra velocità.
Così abbiamo smesso di cercare la risposta giusta. Abbiamo eliminato la domanda.
Il costo delle opzioni
Ogni proprietà visiva in un design system è una superficie decisionale. Il solo border-radius genera domande: pulsanti e card dovrebbero usare lo stesso valore? E per gli elementi annidati — il raggio interno deve sottrarre quello esterno? Il raggio cambia a dimensioni diverse?
Nessuna di queste domande è difficile. Ed è proprio questo che le rende pericolose. Sono abbastanza facili da dare a tutti un’opinione, ma abbastanza irrilevanti da rendere impossibile dimostrare che un’opinione sia migliore di un’altra. Classico bikeshedding.
Abbiamo taggato manualmente i commenti delle PR nell’arco di un trimestre. Circa il 30% dei thread di revisione del nostro design system riguardava valori estetici che non avevano alcun impatto sull’usabilità. Non il layout. Non l’accessibilità. Non i pattern di interazione. Solo… estetica.
È stato allora che abbiamo scritto le regole.
Perché eliminare invece di limitare
L’alternativa ovvia è mantenere le proprietà ma limitarle. La maggior parte dei design system maturi fa così — tre valori consentiti di border-radius, una scala di elevazione delle ombre, una palette di gradienti curata. L’abbiamo considerato.
Il problema è che le opzioni limitate generano comunque dibattiti. “Questa card dovrebbe usare radius-sm o radius-md?” è la stessa conversazione con meno risposte. Meno è meglio di tante, ma zero è meglio di poche. Limitare una proprietà riduce le decisioni. Eliminarla rimuove completamente la decisione.
Abbiamo anche valutato il linting — una regola ESLint che rifiuta i valori non autorizzati. Ma un linter che impone “usa solo 0, 4 o 8px di raggio” lascia comunque tre scelte. L’obiettivo non era rendere la decisione più facile. Era renderla superflua.
Quattro regole
La libreria di componenti di Beaket ha quattro vincoli visivi:
- Nessun border-radius. Angoli netti su tutto. Pulsanti, card, input, dialog, tooltip — tutto rettangolare.
- Nessun gradiente. Solo colori piatti. Ogni superficie è una singola variabile CSS.
- Nessuna ombra sfocata. Nessun
box-shadowcon blur radius. Solo ombre con offset. - Nessuna opacità decorativa. Gli stati disabilitati usano cambi di colore, non trasparenza. Nessuna scorciatoia con
opacity: 0.5.
Queste non sono linee guida. Sono regole rigide, applicate nella code review e documentate nelle nostre linee guida dei componenti. Una PR che aggiunge rounded-md viene rifiutata.
Perché proprio queste quattro proprietà? Perché erano quelle che generavano più dibattito con il minor impatto sull’usabilità. La spaziatura e la tipografia influenzano la leggibilità. Il colore influenza il significato. Ma la differenza tra 4px e 8px di border-radius? Tra un’ombra sfocata di 4px e una di 6px? Non siamo riusciti a trovare un solo caso in cui quelle scelte abbiano influenzato in modo misurabile l’utilizzo del prodotto.
Dopo aver adottato le regole, le PR del nostro design system hanno iniziato a procedere più velocemente. I thread di revisione si sono accorciati. I designer hanno smesso di produrre varianti multiple dello stesso componente che differivano solo per il trattamento degli angoli o la profondità delle ombre. I dibattiti non sono diminuiti — sono scomparsi.
Cosa usiamo al loro posto
Rimuovere opzioni non significa rimuovere la gerarchia visiva. Bisogna comunque comunicare profondità, stato e interazione. Ecco come abbiamo sostituito ogni proprietà.
Ombre con offset invece del blur
Invece di box-shadow: 0 4px 6px rgba(0,0,0,0.1), usiamo ombre con offset definite come variabili CSS:
--shadow-offset: 1px 1px 0px 0px var(--color-chrome);
--shadow-offset-hover: 2px 2px 0px 0px var(--color-chrome);
--shadow-offset-active: 0px 0px 0px 0px var(--color-chrome);
Tre stati di interazione. Zero blur. Questo è l’intero sistema di ombre.
Lo stato predefinito ha un offset di 1px — l’elemento si trova leggermente sopra la superficie. L’hover lo porta a 2px — un leggero sollevamento. L’active lo riporta a 0px — l’elemento si appiattisce. È una piccola animazione meccanica che comunica chiaramente l’interazione e non lascia nulla da configurare allo sviluppatore.
In pratica, ogni componente interattivo applica gli stessi tre token. Ecco la base del nostro componente button (semplificata rispetto alla stringa CVA completa, che include anche gli stati disabled e focus):
const buttonVariants = cva(
[
"inline-flex items-center justify-center gap-2",
"font-medium cursor-pointer",
"shadow-offset hover:shadow-offset-hover active:shadow-offset-active",
"transition-shadow duration-100",
// ... disabled, focus-visible, and icon styles
].join(" "),
// ... variants
);
Nessuna decisione su quale livello di ombra un componente “meriti”.
Colori piatti tramite variabili CSS
La nostra palette di colori — la chiamiamo “Porcelain” — ha 14 tonalità neutre che vanno da graphite (#030509) a paper (#ffffff). Sono fredde e con una sfumatura blu. La descrizione del tema nella nostra documentazione dice: “Precisione industriale. Inciso su pannelli di controllo in lacca sintetica nera. Bianco puro, inchiostro nero-blu freddo, ombre spettrali, accento teal.”
Ogni riferimento cromatico passa attraverso un token semantico:
--color-text-primary: var(--color-graphite);
--color-text-secondary: var(--color-steel);
--color-bg-primary: var(--color-paper);
--color-border-default: var(--color-chrome);
Evitiamo i colori predefiniti di Tailwind come bg-white o text-gray-500. Ogni colore è una variabile con un nome, e ogni variabile corrisponde a un ruolo. Nessuno discute se qualcosa debba essere gray-100 o gray-200 — il token semantico ha già la risposta.
Angoli netti ovunque
Questa non ha bisogno di un sostituto. Semplicemente… non si aggiunge il border-radius.
Ciò che ci ha sorpreso è quanto bene gli angoli netti interagiscano con le ombre offset. Una card arrotondata con un’ombra offset risulta strana — il bordo netto dell’ombra stride con l’angolo morbido. Ma una card con angoli netti e un’ombra offset appare intenzionale. La geometria è coerente. I vincoli si rafforzano a vicenda in modi che non avevamo pienamente previsto.
Il linguaggio visivo diventa coerente per sottrazione. Quando tutto è netto, niente deve giustificare la propria forma.
Dove i vincoli si moltiplicano
Il beneficio più evidente di questo approccio emerge nelle questioni trasversali — i pattern che toccano ogni componente.
Gli stati disabilitati sono notoriamente incoerenti tra le librerie di componenti. Alcune usano l’opacità, alcune cambiano lo sfondo, alcune fanno entrambe le cose. Noi abbiamo un solo pattern, applicato a ogni componente:
disabled:shadow-none disabled:cursor-not-allowed disabled:border-dashed disabled:border-chrome disabled:bg-frost disabled:text-steel
Pulsanti, input, select, checkbox, switch — tutti identici. Ombra rimossa (l’affordance interattiva scompare), bordo tratteggiato, sfondo frost, testo steel. Quando hai visto un componente disabilitato, li hai visti tutti. I vincoli hanno eliminato la tentazione di adattare gli stati disabilitati alla “personalità visiva” di ogni componente. Quando i componenti non hanno personalità visiva, gli stati disabilitati convergono naturalmente.
I focus ring funzionano allo stesso modo. Ogni componente focusable usa:
focus-visible:outline-2 focus-visible:outline-signal-blue focus-visible:outline-offset-2
Un colore. Una larghezza. Un offset. Un utente che naviga con la tastiera attraverso un form vede lo stesso identico ring su ogni elemento. Nessun componente lo sovrascrive. Nessuna variante lo rimuove. Combinato con il pattern uniforme degli stati disabilitati, un utente sa sempre esattamente che aspetto hanno “inattivo” e “con focus” — indipendentemente dal componente con cui sta interagendo.
L’unica eccezione
I radio button usano rounded-full.
È tutto. È l’unica eccezione nell’intera libreria di componenti. È documentata nelle nostre linee guida dei componenti insieme alle regole che infrange, perché anche le eccezioni dovrebbero essere decisioni, non incidenti.
Un radio button quadrato confonderebbe gli utenti. La forma circolare è così profondamente radicata nelle convenzioni delle interfacce che rimuoverla significherebbe sacrificare l’usabilità per la purezza estetica. Abbiamo deciso che era un cattivo compromesso.
Potreste chiedervi: se l’usabilità giustifica un’eccezione per i radio, perché non per gli avatar, i tag o i toggle switch? L’abbiamo testato. Gli avatar quadrati con ombre offset sembrano schede profilo — risultano intenzionali. Tag e toggle sono meno legati alle convenzioni rispetto ai radio button. L’affordance circolare è particolarmente forte per i radio; nient’altro ha superato la soglia.
L’obiettivo non è mai stato “brutalismo fine a se stesso”. Era rimuovere le decisioni visive che non giustificano la loro complessità. Un radio button arrotondato giustifica la sua forma. Una card arrotondata no.
Cosa costa davvero
Saremmo disonesti se dicessimo che questo approccio non ha svantaggi. Ne ha.
“Sembra incompleto.” È una reazione reale emersa dai test con gli utenti. Angoli netti e colori piatti non trasmettono la stessa rifinitura delle interfacce arrotondate e con ombre. Abbiamo dovuto spiegare il ragionamento più di una volta.
Alcuni componenti risultano rigidi. Tooltip e popover, in particolare, appaiono più pesanti con gli angoli netti. Le ombre sfocate esistono per un motivo — creano una sensazione di galleggiamento che le ombre con offset non riescono del tutto a replicare.
I componenti di terze parti rompono il pattern. Date picker, editor di testo ricco, librerie di grafici — tutti arrivano con border-radius e ombre sfocate incorporati. Sovrascriviamo quello che possiamo e accettiamo una certa incoerenza ai margini. Questa è una tensione continua, non un problema risolto.
L’espressione dei designer è limitata. Se siete il tipo di designer che trova gioia nel creare micro-interazioni con overlay sfumati e profondità sottili, questo sistema vi sembrerà limitante. Quello è lo scopo — ma resta un costo reale per la soddisfazione creativa.
È polarizzante. Alcuni ingegneri amano la semplicità. Altri lo trovano dogmatico. Non abbiamo risolto completamente questa tensione. Abbiamo solo deciso che il beneficio di coerenza supera il costo estetico per il nostro prodotto.
Cosa non sappiamo ancora
Non abbiamo testato questo sistema su vera scala — centinaia di componenti, interfacce dense di dati, integrazioni con terze parti che devono apparire native. Tabelle, grafici e dashboard potrebbero aver bisogno di indicatori di profondità che le ombre con offset non possono fornire.
Siamo genuinamente incerti se gli utenti sviluppino la stessa comprensione intuitiva dei nostri stati d’ombra (predefinito → hover → active) che hanno con i sistemi di elevazione convenzionali. Non abbiamo osservato regressioni di usabilità, ma non abbiamo condotto test A/B formali su questo specifico aspetto. Funziona finora. Ma “finora” è un campione piccolo.
I vincoli come decisioni
Ogni vincolo in questo sistema è una decisione presa una volta per non doverla prendere mai più. Nessun border-radius non è una preferenza estetica — è un ADR (Architecture Decision Record) per l’intera libreria di componenti.
Se il vostro team passa tempo a discutere proprietà visive che non influenzano l’usabilità, considerate di eliminare completamente l’opzione. Potrebbe mancarvi meno di quanto pensiate.