Utilizzo e gestione delle icone

Scegliere una tecnica per le icone

L'archiviazione e il rendering delle icone sono scelte separate. Ad esempio, uno sprite SVG può fornire un'icona al sistema di icone CSS o essere referenziato con <use>.

  • Sistema di icone CSS: utilizza questa opzione quando il CSS deve scegliere quando e quale icona mostrare. Utilizza una maschera per un'icona monocolore o un'immagine di sfondo per un'icona a colori.
  • SVG con <use>: utilizza questa opzione per riutilizzare un simbolo quando le opzioni di stile integrate sono sufficienti. Il CSS non può applicare stili o animazioni arbitrari ai contenuti dell'SVG di riferimento, ma le proprietà ereditate e le proprietà personalizzate possono essere passate quando il simbolo è progettato per utilizzarle. Ad esempio, un percorso può utilizzare fill="var(--icon-accent, currentColor)" per consentire al CSS di colorare quella parte dell'icona.
  • SVG incorporato: utilizza questa opzione se hai bisogno di uno stile o di un'animazione arbitrari dei contenuti dell'SVG.
  • <img> con filtri CSS: evita questa opzione per ricolorare le icone. I filtri non possono applicare in modo affidabile un valore di colore CSS arbitrario all'immagine, quindi la ricolorazione è complessa e poco flessibile.

Per decidere, poniti due domande. Il CSS deve scegliere o scambiare l'icona (valori predefiniti dei componenti, modifiche di stato, temi)? In caso affermativo, utilizza il sistema di icone CSS. Devi andare oltre il colore dell'icona? In caso affermativo e se controlli l'origine dell'icona, progetta il simbolo con proprietà personalizzate e utilizza <use>; se non controlli l'origine, inserisci l'SVG in linea. I set di icone di terze parti raramente espongono proprietà personalizzate, quindi <use> ti offre il colore e poco altro.

Icone incorporate o remote

Sia il sistema di icone CSS sia <use> possono acquisire icone da un URI di dati, da un singolo file o da uno sprite. L'incorporamento (URI di dati o SVG incorporato) evita una richiesta e viene visualizzato con il primo paint, ma gonfia il CSS o l'HTML e non può essere memorizzato nella cache separatamente, quindi è adatto a un piccolo insieme di icone che vengono visualizzate su ogni pagina. I singoli file vengono memorizzati nella cache in modo indipendente e caricati on demand, al costo di una richiesta ciascuno. Uno sprite è una richiesta memorizzabile nella cache per l'intero set, ma scarica ogni icona, indipendentemente dal fatto che venga utilizzata o meno, e <use> può fare riferimento solo a uno sprite della stessa origine. Preferisci l'incorporamento per un numero limitato di icone principali e uno sprite o singoli file per set più grandi.

Se una pagina esegue il rendering di migliaia di icone contemporaneamente, esegui il benchmark della pagina e degli asset effettivi. In uno stress test, l'SVG incorporato ha mostrato un leggero vantaggio in termini di prestazioni su questa scala, ma i risultati dipendono dal browser e dall'implementazione. Stress test delle icone SVG di Cloud Four

Approccio principale: motore di icone basato su CSS

Le icone sono token di progettazione: ogni icona è una proprietà personalizzata contenente un'immagine e qualsiasi elemento può visualizzarne una impostando --icon-start o --icon-end. Il motore combina tre funzionalità CSS:

  • Le proprietà personalizzate registrate (@property) impediscono l'ereditarietà di --icon-start e --icon-end, quindi un'icona impostata su un componente non viene applicata ai relativi elementi secondari.
  • Le query sugli stili dei container (@container style(...)) rilevano quando a un elemento è assegnata un'icona e la visualizzano, senza modifiche alla classe o al markup.
  • Le maschere CSS e currentColor eseguono il rendering dell'icona in uno pseudo-elemento che segue il colore del testo dell'elemento. Le dimensioni, il colore, la spaziatura e l'offset verticale vengono letti dalle proprietà --icon-style-* ereditabili con fallback sensibili, in modo che l'intero sottoalbero possa essere ridisegnato da un predecessore. Il prefisso --icon-style- impedisce che queste impostazioni entrino in conflitto con i token delle icone, che si trovano anch'essi in --icon-*.

Definizione dei token di progettazione delle icone

Definisci le icone SVG e gli eventuali override di --icon-style-* come proprietà personalizzate riutilizzabili utilizzando URI di dati SVG incorporati o URL relativi alla directory principale (ad es. url("/icons/trash.svg")). Quando un'icona viene caricata da un file SVG esterno, utilizza un URL relativo alla directory principale o un URI di dati incorporato. Un URL relativo utilizzato tramite una proprietà personalizzata può essere risolto in relazione al foglio di stile in cui viene utilizzata l'icona, anziché al foglio di stile in cui è definita la proprietà personalizzata. Se questi fogli di stile si trovano in directory diverse, l'URL potrebbe rimandare al file sbagliato.

Per mantenere un URL relativo al foglio di stile che definisce il token dell'icona, è utile una proprietà registrata: il valore calcolato di un <url> o <image> registrato viene risolto in base al foglio di stile che lo ha dichiarato. NON fare affidamento sulla registrazione del token come <image> con l'URL in initial-value. Chrome e Safari risolvono gli URL initial-value in base al documento, quindi funziona solo in Firefox. Chrome risolve anche i valori dichiarati su :root rispetto al documento, a meno che la sintassi non inizi con <url>.

Quando gli URL relativi sono inevitabili, ad esempio i fogli di stile in directory diverse, registra il token con una sintassi che inizia con <url> e dichiara l'URL effettivo su :root. Questo viene risolto rispetto al foglio di stile di dichiarazione in ogni browser:

@property --icon-caret {
  /* The syntax must start with <url>. A syntax that starts with <image> resolves against the document in Chrome. */
  syntax: "<url> | <image> | none";
  inherits: true;
  initial-value: none;
}

:root {
  /* Declared here, the URL resolves against this stylesheet */
  --icon-caret: url("icons/caret.svg");
}

Ogni icona ora si trova in due posizioni e c'è molto più boilerplate, quindi vale la pena farlo solo quando un progetto ha fogli di stile in directory diverse e un numero gestibile di icone. Se hai più di una dozzina di icone, registra i token --icon-* (escluso --icon-style-*) da uno script con CSS.registerProperty, utilizzando la stessa sintassi, anziché scrivere ogni @property a mano. Le proprietà registrate sono globali e non funzionano negli shadow root, quindi potrebbero non essere adatte a tutti i progetti. Se gli URL relativi alla directory principale o gli URI dei dati sono un'opzione, sono più semplici e funzionano ovunque.

:root {
  /* Optional: override the built-in fallbacks (1em, currentColor, 0.3em, -0.02em) */
  --icon-style-size: 1em;
  --icon-style-spacing: 0.4em;
  --icon-trash: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0-1 14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2L4 6h16Z'/%3E%3C/svg%3E");
  --icon-favorite: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E");
}

Rendering delle icone dai token

Un elemento visualizza un'icona impostando --icon-start (iniziale) o --icon-end (finale) su uno dei token sopra indicati. Due parti di CSS trasformano l'assegnazione in un'icona visualizzata.

Per prima cosa, registra --icon-start e --icon-end con @property. La sintassi "*" accetta qualsiasi valore e inherits: false è la parte importante: senza di essa, un set di icone su un pulsante verrebbe ereditato da ogni elemento secondario e ognuno di questi elementi visualizzerebbe anche l'icona.

@property --icon-start {
  syntax: "*";
  inherits: false;
}

@property --icon-end {
  syntax: "*";
  inherits: false;
}

In secondo luogo, una query di stile del contenitore rileva quando la proprietà è impostata su un elemento ed esegue il rendering dell'icona in uno pseudo-elemento. Poiché lo pseudo-elemento non eredita --icon-start (è stato registrato come non ereditante), la prima dichiarazione inoltra il valore in modo esplicito con inherit. Le dichiarazioni rimanenti gestiscono il dimensionamento, il mascheramento, l'allineamento e la spaziatura. Poiché il riempimento predefinito è currentColor, l'icona adotta il colore del testo dell'elemento e segue le sue transizioni.

/* Style query for elements configuring a start icon */
@container style(--icon-start) {
  ::before {
    --icon-start: inherit; /* Forward the value inside the query block */
    content: "" / var(--icon-style-alt, "");
    display: inline-block;
    width: var(--icon-style-size, 1em);
    height: var(--icon-style-size, 1em);
    background-color: var(--icon-style-color, currentColor);
    mask: var(--icon-start) no-repeat center / contain;
    /* Center on the cap height, nudged slightly up for optical balance */
    vertical-align: calc(0.5cap - var(--icon-style-size, 1em) / 2 + var(--icon-style-voffset, -0.02em));
    flex: none;
    pointer-events: none;
    forced-color-adjust: none;
  }

  /* Automatically add spacing ONLY if the element has other text or sibling content */
  :where(:not(.icon, :empty))::before {
    margin-inline-end: var(--icon-style-spacing, 0.3em);
  }
}

Ogni dichiarazione ha il suo posto:

  • vertical-align: middle si allinea alla metà dell'altezza x, che è visibilmente bassa per la maggior parte dei set di icone. La centratura su 0.5cap mantiene fisso il centro dell'icona man mano che --icon-style-size cresce, mentre il piccolo valore negativo --icon-style-voffset compensa l'occhio che pesa lo spazio sopra un'icona più dello spazio sottostante.
  • flex: none: uno pseudo-elemento vuoto ha una dimensione minima pari a zero, quindi all'interno di un pulsante flessibile è la prima cosa a ridursi quando l'etichetta è lunga, potenzialmente fino a scomparire. La dimensione delle icone è una decisione di progettazione, non qualcosa che deve essere negoziato dalla pressione del layout.
  • pointer-events: none: impedisce all'icona di intercettare i clic o di diventare l'event.target.
  • forced-color-adjust: none: in modalità colori forzati, il browser sostituisce background-color, facendo scomparire un'icona con maschera. Il riempimento della maschera viene conservato e currentColor segue comunque il colore del testo forzato.
  • content: "" / var(--icon-style-alt, ""): il testo alternativo per i contenuti generati consente a un controllo di sola icona di prendere il nome accessibile dall'icona tramite --icon-style-alt (vedi Controlli di sola icona di seguito). L'attributo alt vuoto predefinito mantiene le icone decorative nascoste alle tecnologie per la disabilità.

Per un'icona finale, segui lo stesso pattern con --icon-end e ::after anziché --icon-start e ::before. Utilizza mask: var(--icon-end) e applica la spaziatura con margin-inline-start anziché margin-inline-end.

Elementi autonomi e solo con icone

Per le icone autonome o i pulsanti di sola icona, applica .icon direttamente all'elemento vuoto (il pulsante stesso per un controllo di sola icona) e imposta la relativa icona con --icon. In questo caso, --icon è la proprietà di livello superiore; la regola .icon:empty la mappa a --icon-start per un'icona iniziale:

.icon:empty {
  --icon-start: var(--icon);
}

Esempi di utilizzo

Valori predefiniti dei componenti nel foglio di stile

Il vantaggio principale delle icone come proprietà personalizzate è che partecipano alla cascata. Imposta un'icona su un selettore e ogni elemento corrispondente la riceverà, senza modifiche al markup:

.button-delete {
  --icon-start: var(--icon-trash);
}

a[href^="http"]:not([href*="example.com"]) {
  --icon-end: var(--icon-external);
}
<button class="button-delete">Delete item</button>

Modifica delle icone in base allo stato

Poiché l'icona è solo una proprietà, una modifica dello stato è un normale override CSS. Non esiste un secondo elemento da mostrare o nascondere e nessuna classe da attivare/disattivare:

.button-expand {
  --icon-start: var(--icon-caret-right);
}

.button-expand[aria-expanded="true"] {
  --icon-start: var(--icon-caret-down);
}

Lo stesso vale per la presentazione: imposta --icon-style-size, --icon-style-color o --icon-style-spacing su un contenitore come una barra degli strumenti e ogni icona al suo interno si aggiorna.

Icone uniche con stili incorporati

Un elemento non coperto da nessuna regola del componente può impostare la stessa proprietà in linea, ad esempio <button style="--icon-start: var(--icon-trash)">. Considera questa opzione come una soluzione di emergenza piuttosto che come l'impostazione predefinita: se la stessa icona appare più di una volta sullo stesso tipo di elemento, spostala in una regola del foglio di stile.

Controlli solo icona

I pulsanti di sola icona utilizzano la classe .icon e --icon descritti sopra. Assegna al pulsante un nome accessibile con aria-label o lascia che l'icona ne fornisca uno tramite --icon-style-alt. Senza nessuno dei due, l'icona generata dal CSS è decorativa. Per le icone informative autonome, esponi l'elemento .icon con role="img" e aria-label:

.button-close {
  --icon: var(--icon-close);
  --icon-style-alt: "Close";
}
<!-- Icon-only interactive control, icon and name from the stylesheet -->
<button class="icon button-close"></button>

<!-- Icon-only interactive control, named via aria-label -->
<button class="icon" style="--icon: var(--icon-trash);" aria-label="Delete item"></button>

<!-- Standalone informative icon -->
<span class="icon" style="--icon: var(--icon-favorite);" role="img" aria-label="Favorite"></span>

Lavorare direttamente con SVG

Quando il sistema di icone CSS non è adatto, utilizza l'SVG incorporato e <use> insieme, anziché come alternative. Inserisci l'SVG in linea la prima volta che un'icona appare su una pagina e fai riferimento con <use href="#id"> per ogni ripetizione; il riferimento locale non comporta alcuna richiesta e non ha limitazioni multiorigine. Prenota uno sprite esterno (<use href="/icons.svg#id">) per le icone condivise su più pagine.

Con <use>, i contenuti di riferimento vengono clonati in uno shadow tree, pertanto il CSS della pagina non può selezionare i relativi elementi. Ciò che raggiunge è l'ereditarietà: color, currentColor e qualsiasi proprietà personalizzata. Oltre agli attributi di presentazione SVG, i CSS moderni possono modificare anche molti attributi di geometria, quindi un'icona che controlli può esporre esattamente gli hook di cui hai bisogno, ad esempio stroke-width="var(--icon-stroke, 2)" o anche d: var(--icon-path, path(…)) su un percorso specifico.

La proprietà CSS d non è supportata in Safari, quindi utilizza questa personalizzazione solo quando questa lacuna di supporto è accettabile. Se non controlli l'icona, inseriscila in linea.

<use> insidie da conoscere:

  • viewBox deve trovarsi su <symbol> o su <svg> di riferimento; <svg> esterno che racchiude <use> non lo rileva dal riferimento, quindi imposta width e height sul wrapper e mantieni viewBox con il simbolo.
  • Gli stili non superano il confine in nessuna direzione: <style> all'interno di uno sprite esterno non si applica alla pagina e i fogli di stile della pagina non si applicano all'interno dell'albero di riferimento. Vengono trasmesse solo le proprietà ereditate e quelle personalizzate.
  • I riferimenti esterni sono solo della stessa origine; gli sprite multiorigine non vengono visualizzati.
  • Gli sprite incorporati nel documento condividono lo spazio dei nomi id della pagina, quindi gli ID dei simboli dello spazio dei nomi evitano collisioni.

Contrassegna gli SVG decorativi con aria-hidden="true" e assegna al controllo contenitore un nome accessibile, come nel sistema di icone CSS.

Strategie di fallback

Proprietà personalizzate registrate

Senza @property, --icon-start e --icon-end si comportano come normali proprietà personalizzate ereditate, quindi un'icona impostata su un elemento viene visualizzata anche sui relativi discendenti. Per evitare questo problema, reimposta le proprietà degli elementi secondari all'interno della query di stile (@container style(--icon-start) { * { --icon-start: initial; } }), lo stesso approccio utilizzato dalla soluzione alternativa di Safari riportata di seguito.

Query di stile del contenitore

Per supportare i browser senza query di stile del container, utilizza classi o selettori di componenti come hook di riserva. Mantieni la selezione delle icone nelle proprietà personalizzate; i selettori di stile incorporati da soli non rilevano le icone assegnate tramite regole CSS.

:where(.icon-start, .icon:empty)::before {
  content: image-set(linear-gradient(transparent 0 100%), var(--icon-start)) / var(--icon-style-alt, "");
  /* icon styles */
}

:where(.icon-start)::before {
  --icon-start: inherit;
}

:where(.icon:empty)::before {
  --icon-start: var(--icon);
}

:where(.icon-start:not(.icon, :empty))::before {
  margin-inline-end: var(--icon-style-spacing, 0.3em);
}

Per le icone finali, utilizza lo stesso hook di classe di riserva con .icon-end, --icon-end e ::after anziché .icon-start, --icon-start e ::before. Applica la spaziatura ai contenuti non vuoti con margin-inline-start anziché margin-inline-end.

Questo pattern image-set() rende content non valido quando la proprietà personalizzata non è impostata, quindi torna a none e lo pseudo-elemento scompare senza aggiungere o rimuovere una classe. Non rimuove gli altri stili dello pseudo-elemento né impedisce conflitti con altre regole ::before o ::after, quindi utilizza questi hook solo dove questi pseudo-elementi sono disponibili per le icone.

Soluzioni alternative per il browser Safari

Safari presenta due bug di rendering che interessano le icone: WebKit #301609 può impedire la loro visualizzazione fino all'interazione dell'utente e #320220 influisce anche sul loro rendering. La soluzione alternativa riportata di seguito risolve entrambi i problemi:

@keyframes webkit-301609 {}

@supports ((-webkit-nbsp-mode: normal) or (-webkit-touch-callout: none) or (font: -apple-system-body)) and (content-visibility: auto) {
  @layer webkit-301609-fix {
    ::before, ::after {
      animation: webkit-301609 0s;
    }
  }
}

La query @supports ha come target solo Safari, mentre @layer riduce i conflitti di animazione.

Il bug WebKit n. 325900 può impedire l'applicazione di un override diretto di --icon-start in alcuni casi, ad esempio la modifica dell'icona al passaggio del mouse. Ciò influisce sulla soluzione alternativa per le proprietà registrate non ereditate. Per Safari, registra --icon-start come ereditato e reimpostalo sui discendenti all'interno della query di stile:

@supports ((-webkit-nbsp-mode: normal) or
           (-webkit-touch-callout: none) or
           (font: -apple-system-body)) and (content-visibility: auto) {
  /* Override previous declaration */
  @property --icon-start {
    syntax: "*";
    inherits: true;
  }

  @layer webkit-325900-fix {
    @container style(--icon-start) {
      * {
        --icon-start: initial;
      }
    }
  }
}

La reimpostazione impedisce la propagazione del valore dell'icona ai discendenti. Se lo mantieni in un livello, le dichiarazioni delle icone senza livelli hanno la precedenza sulla reimpostazione.