Symbole verwenden und verwalten

Auswahl einer Symboltechnik

Die Speicherung und das Rendering von Symbolen sind separate Optionen. Beispielsweise kann ein SVG-Sprite ein Symbol für das CSS-Symbolsystem bereitstellen oder mit <use> referenziert werden.

  • CSS-Symbolsystem: Verwenden Sie diese Option, wenn CSS entscheiden soll, wann und welches Symbol angezeigt wird. Verwenden Sie eine Maske für ein einfarbiges Symbol oder ein Hintergrundbild für ein farbiges Symbol.
  • SVG mit <use>: Verwenden Sie diese Option, um ein Symbol wiederzuverwenden, wenn die integrierten Stiloptionen ausreichen. Mit CSS können die Inhalte der referenzierten SVG nicht beliebig formatiert oder animiert werden. Wenn das Symbol jedoch so konzipiert ist, dass es sie verwendet, können übernommene und benutzerdefinierte Eigenschaften übergeben werden. Ein Pfad kann beispielsweise fill="var(--icon-accent, currentColor)" verwenden, damit CSS diesen Teil des Symbols einfärben kann.
  • SVG-Inline-Bild: Verwenden Sie diese Option, wenn Sie beliebige Stile oder Animationen für den Inhalt des SVG-Bilds benötigen.
  • <img> mit CSS-Filtern: Vermeide dies, um Symbole umzufärben. Mit Filtern lässt sich ein beliebiger CSS-Farbwert nicht zuverlässig auf das Bild anwenden. Das Umfärben ist daher umständlich und unflexibel.

Stellen Sie sich zwei Fragen, um eine Entscheidung zu treffen. Muss das CSS das Symbol auswählen oder austauschen (Komponentenstandards, Statusänderungen, Designs)? Falls ja, verwenden Sie das CSS-Symbolsystem. Müssen Sie über die Farbe hinaus in das Symbol hineingreifen? Wenn ja und Sie die Symbolquelle steuern, entwerfen Sie das Symbol mit benutzerdefinierten Eigenschaften und verwenden Sie <use>. Wenn Sie die Quelle nicht steuern, binden Sie die SVG-Datei inline ein. Symbolsätze von Drittanbietern stellen selten benutzerdefinierte Eigenschaften bereit. Wenn Sie <use> für sie verwenden, erhalten Sie also nur die Farbe und wenig anderes.

Eingebettete oder Remote-Symbole

Sowohl das CSS-Symbolsystem als auch <use> können Symbole aus einem Daten-URI, einer einzelnen Datei oder einem Sprite übernehmen. Durch das Einbetten (Daten-URIs oder Inline-SVG) wird eine Anfrage vermieden und die Symbole werden beim ersten Rendern dargestellt. Allerdings wird dadurch CSS oder HTML aufgebläht und die Symbole können nicht separat im Cache gespeichert werden. Daher eignet sich diese Methode für eine kleine Anzahl von Symbolen, die auf jeder Seite angezeigt werden. Einzelne Dateien werden unabhängig voneinander im Cache gespeichert und bei Bedarf geladen. Dabei wird jeweils eine Anfrage berechnet. Ein Sprite ist eine im Cache speicherbare Anfrage für den gesamten Satz, aber es werden alle Symbole heruntergeladen, unabhängig davon, ob sie verwendet werden oder nicht. Außerdem kann <use> nur auf ein Sprite aus derselben Quelle verweisen. Für eine Handvoll wichtiger Symbole ist das Einbetten besser geeignet, für größere Mengen ein Sprite oder einzelne Dateien.

Wenn auf einer Seite Tausende von Symbolen gleichzeitig gerendert werden, sollten Sie die tatsächliche Seite und die Assets einem Benchmark-Test unterziehen. In einem Stresstest hatte Inline-SVG bei dieser Größe einen kleinen Leistungsvorteil, aber die Ergebnisse hängen vom Browser und der Implementierung ab. SVG-Symbol-Stresstest von Cloud Four

Primärer Ansatz: CSS-gesteuerte Symbol-Engine

Symbole sind Design-Tokens: Jedes Symbol ist eine benutzerdefinierte Eigenschaft, die ein Bild enthält. Jedes Element kann ein Symbol anzeigen, indem --icon-start oder --icon-end festgelegt wird. Die Engine kombiniert drei CSS-Funktionen:

  • Registrierte benutzerdefinierte Eigenschaften (@property) verhindern, dass --icon-start und --icon-end übernommen werden. Ein Symbol, das für eine Komponente festgelegt wurde, wird also nicht an ihre untergeordneten Elemente weitergegeben.
  • Mit Container-Stilabfragen (@container style(...)) wird erkannt, wenn einem Element ein Symbol zugewiesen ist, und es wird ohne Änderungen an der Klasse oder am Markup gerendert.
  • CSS-Masken und currentColor rendern das Symbol in einem Pseudoelement, das der Textfarbe des Elements folgt. Größe, Farbe, Abstand und vertikaler Versatz werden aus vererbbaren --icon-style-*-Eigenschaften mit sinnvollen Fallbacks gelesen, sodass ein ganzer Unterbaum von einem Vorgänger neu formatiert werden kann. Das Präfix --icon-style- verhindert, dass diese Einstellungen mit den Symbol-Tokens selbst kollidieren, die sich ebenfalls unter --icon-* befinden.

Design-Tokens für Symbole definieren

Definieren Sie SVG-Symbole und alle --icon-style-*-Überschreibungen als wiederverwendbare benutzerdefinierte Eigenschaften mit Inline-SVG-Daten-URIs oder relativen URLs (z.B. url("/icons/trash.svg")). Wenn ein Symbol aus einer externen SVG-Datei geladen wird, verwenden Sie eine relative URL oder einen eingebetteten Daten-URI. Eine relative URL, die über eine benutzerdefinierte Eigenschaft verwendet wird, kann relativ zu dem Stylesheet aufgelöst werden, in dem das Symbol verwendet wird, und nicht zu dem Stylesheet, in dem die benutzerdefinierte Eigenschaft definiert ist. Wenn sich diese Stylesheets in verschiedenen Verzeichnissen befinden, wird die URL möglicherweise in die falsche Datei aufgelöst.

Damit eine URL relativ zum Stylesheet bleibt, das das Symbol-Token definiert, soll eine registrierte Property helfen: Der berechnete Wert einer registrierten <url> oder <image> wird anhand des Stylesheets aufgelöst, in dem sie deklariert wurde. Verlassen Sie sich NICHT darauf, das Token als <image> mit der URL in initial-value zu registrieren. Chrome und Safari lösen initial-value-URLs anhand des Dokuments auf, sodass dies nur in Firefox funktioniert. Chrome löst auch Werte auf, die für :root deklariert wurden, sofern die Syntax nicht mit <url> beginnt.

Wenn relative URLs unvermeidbar sind, z. B. bei Stylesheets in verschiedenen Verzeichnissen, registrieren Sie das Token mit einer Syntax, die mit <url> beginnt, und deklarieren Sie die tatsächliche URL in :root. Dies wird in jedem Browser anhand des deklarierenden Stylesheets aufgelöst:

@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");
}

Jedes Symbol befindet sich jetzt an zwei Stellen und es gibt viel mehr Boilerplate. Das lohnt sich also nur, wenn ein Projekt Stylesheets in verschiedenen Verzeichnissen und eine überschaubare Anzahl von Symbolen hat. Wenn Sie mehr als ein Dutzend Symbole haben, registrieren Sie die --icon-*-Tokens (außer --icon-style-*) aus einem Skript mit CSS.registerProperty und verwenden Sie dabei dieselbe Syntax, anstatt jedes @property manuell zu schreiben. Registrierte Properties sind global und funktionieren nicht in Shadow Roots. Daher sind sie möglicherweise nicht für jedes Projekt geeignet. Wenn Stamm-relative URLs oder Daten-URIs eine Option sind, sind sie einfacher und funktionieren überall.

: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");
}

Symbole aus Tokens rendern

Ein Element zeigt ein Symbol an, indem --icon-start (führend) oder --icon-end (nachgestellt) auf eines der oben genannten Tokens gesetzt wird. Zwei CSS-Elemente wandeln diese Zuweisung in ein gerendertes Symbol um.

Registrieren Sie zuerst --icon-start und --icon-end bei @property. Die Syntax "*" akzeptiert jeden Wert. inherits: false ist der wichtige Teil: Ohne ihn würde ein auf einer Schaltfläche festgelegtes Symbol an alle untergeordneten Elemente vererbt und jedes von ihnen würde das Symbol ebenfalls rendern.

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

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

Zweitens wird mit einer Containerstil-Abfrage erkannt, wenn die Property für ein Element festgelegt ist, und das Symbol wird in einem Pseudoelement gerendert. Da das Pseudoelement --icon-start nicht übernimmt (es wurde als nicht übernehmend registriert), wird der Wert in der ersten Deklaration explizit mit inherit weitergeleitet. Die verbleibenden Deklarationen betreffen Größe, Maskierung, Ausrichtung und Abstände. Da die Füllung standardmäßig auf currentColor festgelegt ist, übernimmt das Symbol die Textfarbe des Elements und folgt seinen Übergängen.

/* 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);
  }
}

Jede Deklaration hat ihren Platz:

  • vertical-align: middle wird an der Hälfte der x-Höhe ausgerichtet, was bei den meisten Symbolsets sichtbar niedrig ist. Wenn Sie das Symbol auf 0.5cap zentrieren, bleibt die Mitte des Symbols fixiert, während --icon-style-size wächst. Der kleine negative --icon-style-voffset-Wert gleicht aus, dass das Auge den Bereich über einem Symbol stärker gewichtet als den Bereich darunter.
  • flex: none: Ein leeres Pseudoelement hat eine Mindestgröße von null. In einem Flex-Button ist es daher das erste Element, das verkleinert wird, wenn das Label lang ist, möglicherweise bis auf null. Die Symbolgröße ist eine Designentscheidung und sollte nicht durch Layoutdruck beeinflusst werden.
  • pointer-events: none: Verhindert, dass das Symbol Klicks abfängt oder zum event.target wird.
  • forced-color-adjust: none: Im Modus für erzwungene Farben ersetzt der Browser sonst background-color, wodurch ein maskiertes Symbol verschwindet. Die Maskenfüllung bleibt erhalten und currentColor folgt weiterhin der erzwungenen Textfarbe.
  • content: "" / var(--icon-style-alt, ""): Alternativer Text für generierte Inhalte ermöglicht es einem Steuerelement, das nur aus einem Symbol besteht, seinen barrierefreien Namen über --icon-style-alt vom Symbol zu übernehmen (siehe „Steuerelemente, die nur aus einem Symbol bestehen“ unten). Durch das standardmäßig leere Alt-Attribut werden dekorative Symbole für Hilfstechnologien ausgeblendet.

Für ein nachgestelltes Symbol folgen Sie demselben Muster mit --icon-end und ::after anstelle von --icon-start und ::before. Verwenden Sie mask: var(--icon-end) und wenden Sie Abstände mit margin-inline-start anstelle von margin-inline-end an.

Eigenständige und reine Symbolelemente

Wenden Sie bei eigenständigen Symbolen oder Schaltflächen, die nur aus Symbolen bestehen, .icon direkt auf das leere Element an (die Schaltfläche selbst für ein Steuerelement, das nur aus Symbolen besteht) und legen Sie das Symbol mit --icon fest. Hier ist --icon die übergeordnete Property. Die Regel .icon:empty ordnet sie --icon-start für ein führendes Symbol zu:

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

Anwendungsbeispiele

Komponentenstandards im Stylesheet

Der Hauptvorteil von Symbolen als benutzerdefinierte Eigenschaften besteht darin, dass sie in der Kaskade berücksichtigt werden. Wenn Sie ein Symbol für einen Selektor festlegen, wird es für jedes übereinstimmende Element übernommen, ohne dass Änderungen am Markup erforderlich sind:

.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>

Symbole je nach Status ändern

Da das Symbol nur ein Attribut ist, ist eine Statusänderung eine gewöhnliche CSS-Überschreibung. Es gibt kein zweites Element, das ein- oder ausgeblendet werden muss, und keine Klasse, die umgeschaltet werden muss:

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

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

Dasselbe gilt für die Darstellung: Wenn Sie --icon-style-size, --icon-style-color oder --icon-style-spacing für einen Container wie eine Symbolleiste festlegen, werden alle Symbole darin aktualisiert.

Einmalige Symbole mit Inline-Stilen

Ein Element, das von keiner Komponentenregel abgedeckt wird, kann dasselbe Attribut inline festlegen, z. B. <button style="--icon-start: var(--icon-trash)">. Betrachten Sie dies als Notlösung und nicht als Standard: Sobald dasselbe Symbol mehr als einmal auf demselben Elementtyp erscheint, verschieben Sie es in eine Stylesheet-Regel.

Nur Symbole anzeigen

Für Schaltflächen, die nur ein Symbol enthalten, werden die oben beschriebene Klasse .icon und --icon verwendet. Geben Sie der Schaltfläche mit aria-label einen barrierefreien Namen oder lassen Sie das Symbol einen über --icon-style-alt bereitstellen. Ohne diese Angaben ist das CSS-generierte Symbol dekorativ. Für eigenständige informative Symbole stellen Sie das .icon-Element mit role="img" und aria-label bereit:

.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>

Direkt mit SVG arbeiten

Wenn das CSS-Symbolsystem nicht geeignet ist, verwende Inline-SVG und <use> zusammen, anstatt sie als Alternativen zu verwenden. Fügen Sie die SVG-Datei inline ein, wenn ein Symbol zum ersten Mal auf einer Seite erscheint, und verweisen Sie bei jeder Wiederholung mit <use href="#id"> darauf. Der lokale Verweis kostet keine Anfrage und unterliegt keiner Cross-Origin-Einschränkung. Reservieren Sie ein externes Sprite (<use href="/icons.svg#id">) für Symbole, die auf vielen Seiten verwendet werden.

Bei <use> wird der referenzierte Inhalt in einen Shadow-Baum geklont, sodass die Elemente nicht über das CSS der Seite ausgewählt werden können. Was wird vererbt: color, currentColor und alle benutzerdefinierten Eigenschaften. Neben SVG-Präsentationsattributen können mit modernem CSS auch viele Geometrieattribute geändert werden. So kann ein von Ihnen gesteuertes Symbol genau die Hooks bereitstellen, die Sie benötigen, z. B. stroke-width="var(--icon-stroke, 2)" oder sogar d: var(--icon-path, path(…)) für einen bestimmten Pfad.

CSS-d wird in Safari nicht unterstützt. Verwenden Sie diese Anpassung daher nur, wenn diese Unterstützungslücke akzeptabel ist. Wenn Sie das Symbol nicht kontrollieren, binden Sie es inline ein.

<use> wichtige Hinweise:

  • Das viewBox muss sich auf dem <symbol> oder dem referenzierten <svg> befinden. Das äußere <svg>, das das <use> umschließt, übernimmt es nicht aus der Referenz. Setzen Sie daher width und height auf den Wrapper und behalten Sie das viewBox mit dem Symbol bei.
  • Stile werden nicht in die eine oder andere Richtung übertragen: <style> innerhalb eines externen Sprites gilt nicht für die Seite und Seiten-Stylesheets gelten nicht innerhalb des referenzierten Baums. Nur übernommene und benutzerdefinierte Eigenschaften werden weitergegeben.
  • Externe Verweise sind nur für dieselbe Quelle zulässig. Ursprungsübergreifende Sprites werden ohne Fehlermeldung ignoriert.
  • Sprites, die inline in das Dokument eingefügt werden, teilen sich den Namespace id der Seite. Verwenden Sie daher Namespace-Symbol-IDs, um Konflikte zu vermeiden.

Markieren Sie dekorative SVGs mit aria-hidden="true" und geben Sie dem enthaltenden Steuerelement einen eigenen barrierefreien Namen, wie beim CSS-Symbolsystem.

Fallback-Strategien

Registrierte benutzerdefinierte Eigenschaften

Ohne @property verhalten sich --icon-start und --icon-end wie gewöhnliche vererbte benutzerdefinierte Eigenschaften. Ein Symbol, das für ein Element festgelegt wurde, wird also auch für seine untergeordneten Elemente gerendert. Sie können dies verhindern, indem Sie die Eigenschaften der untergeordneten Elemente innerhalb der Stilabfrage (@container style(--icon-start) { * { --icon-start: initial; } }) zurücksetzen. Dies ist derselbe Ansatz, der auch im Safari-Workaround unten verwendet wird.

Container-Stilabfragen

Verwenden Sie für Browser ohne Container-Stilabfragen Klassen oder Komponentenselektoren als Fallback-Hooks. Behalten Sie die Symbolauswahl in benutzerdefinierten Eigenschaften bei. Wenn Sie nur Inline-Stil-Selektoren verwenden, werden Symbole, die über CSS-Regeln zugewiesen wurden, nicht berücksichtigt.

: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);
}

Verwenden Sie für nachgestellte Symbole denselben Klassen-Hook-Fallback mit .icon-end, --icon-end und ::after anstelle von .icon-start, --icon-start und ::before. Wenden Sie Abstände auf nicht leere Inhalte mit margin-inline-start anstelle von margin-inline-end an.

Dieses image-set()-Muster macht content ungültig, wenn die benutzerdefinierte Eigenschaft nicht festgelegt ist. Daher wird auf none zurückgegriffen und das Pseudoelement verschwindet, ohne dass eine Klasse hinzugefügt oder entfernt wird. Die anderen Stile des Pseudoelements werden nicht entfernt und Konflikte mit anderen ::before- oder ::after-Regeln werden nicht verhindert. Verwenden Sie diese Hooks daher nur, wenn diese Pseudoelemente für Symbole verfügbar sind.

Workarounds für den Safari-Browser

In Safari gibt es zwei Rendering-Fehler, die sich auf Symbole auswirken: WebKit #301609 kann verhindern, dass sie bis zur Nutzerinteraktion angezeigt werden, und #320220 wirkt sich ebenfalls auf das Rendering aus. Die folgende Problemumgehung behebt beide Probleme:

@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;
    }
  }
}

Die Abfrage @supports ist nur für Safari vorgesehen und @layer reduziert Animationskonflikte.

Der WebKit-Fehler 325900 kann in einigen Fällen verhindern, dass eine direkte Überschreibung von --icon-start wirksam wird, z. B. wenn das Symbol beim Bewegen des Mauszeigers geändert wird. Dies wirkt sich auf die Problemumgehung für nicht übernommene registrierte Properties aus. Registrieren Sie für Safari --icon-start als Übernahme und setzen Sie es für untergeordnete Elemente innerhalb der Stilabfrage zurück:

@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;
      }
    }
  }
}

Durch das Zurücksetzen wird verhindert, dass der Symbolwert an untergeordnete Elemente weitergegeben wird. Wenn Sie es in einer Ebene belassen, haben Symboldeklarationen ohne Ebene Vorrang vor dem Zurücksetzen.