Używanie ikon i zarządzanie nimi

Wybieranie techniki tworzenia ikon

Przechowywanie i renderowanie ikon to osobne opcje. Na przykład sprite SVG może dostarczać ikonę do systemu ikon CSS lub być przywoływany za pomocą <use>.

  • System ikon CSS: używaj, gdy CSS ma decydować, kiedy i którą ikonę wyświetlić. Użyj maski w przypadku ikony jednokolorowej lub obrazu tła w przypadku ikony pełnokolorowej.
  • SVG z <use>: użyj, aby ponownie użyć symbolu, gdy jego wbudowane opcje stylizacji są wystarczające. CSS nie może dowolnie stylizować ani animować zawartości przywoływanego pliku SVG, ale właściwości dziedziczone i właściwości niestandardowe mogą być przekazywane, gdy symbol jest zaprojektowany do ich używania. Na przykład ścieżka może używać fill="var(--icon-accent, currentColor)", aby umożliwić CSS pokolorowanie tej części ikony.
  • Wbudowany SVG: używaj, gdy potrzebujesz dowolnego stylu lub animacji zawartości SVG.
  • <img> z filtrami CSS: unikaj tego rozwiązania w przypadku zmiany koloru ikon. Filtry nie mogą niezawodnie zastosować do obrazu dowolnej wartości koloru CSS, więc zmiana koloru jest uciążliwa i nieelastyczna.

Aby podjąć decyzję, zadaj sobie 2 pytania. Czy CSS musi wybrać lub zamienić ikonę (domyślne komponenty, zmiany stanu, motywy)? Jeśli tak, użyj systemu ikon CSS. Czy chcesz, aby ikona przekazywała więcej informacji niż tylko kolor? Jeśli tak i masz kontrolę nad źródłem ikony, zaprojektuj symbol z właściwościami niestandardowymi i użyj <use>. Jeśli nie masz kontroli nad źródłem, umieść SVG w kodzie. Zestawy ikon innych firm rzadko udostępniają właściwości niestandardowe, więc <use> w ich przypadku daje tylko kolor i niewiele więcej.

Ikony umieszczone lub zdalne

Zarówno system ikon CSS, jak i <use> mogą pobierać ikony z identyfikatora URI danych, pojedynczego pliku lub sprite'a. Osadzanie (identyfikatory URI danych lub wbudowane pliki SVG) pozwala uniknąć żądania i renderuje się przy pierwszym wyrenderowaniu, ale zwiększa rozmiar kodu CSS lub HTML i nie można go zapisać w osobnej pamięci podręcznej, więc nadaje się do małego zestawu ikon, które pojawiają się na każdej stronie. Poszczególne pliki są przechowywane w pamięci podręcznej niezależnie i wczytywane na żądanie, co kosztuje jedno żądanie za każdy plik. Duszek to jedno żądanie z możliwością zapisu w pamięci podręcznej dla całego zestawu, ale pobiera każdą ikonę, niezależnie od tego, czy jest używana, czy nie, a <use> może odwoływać się tylko do duszka z tego samego źródła. W przypadku kilku podstawowych ikon zalecamy osadzanie, a w przypadku większych zestawów – sprite lub pojedyncze pliki.

Jeśli strona renderuje tysiące ikon naraz, przeprowadź test porównawczy rzeczywistej strony i zasobów. W teście obciążeniowym wbudowany kod SVG miał niewielką przewagę wydajnościową w tej skali, ale wyniki zależą od przeglądarki i implementacji. Test obciążeniowy ikon SVG przeprowadzony przez Cloud Four

Podstawowe podejście: mechanizm ikon oparty na CSS

Ikony to tokeny projektowe: każda ikona to właściwość niestandardowa zawierająca obraz, a każdy element może wyświetlać ikonę, ustawiając --icon-start lub --icon-end. Silnik łączy 3 funkcje CSS:

  • Zarejestrowane właściwości niestandardowe (@property) uniemożliwiają dziedziczenie właściwości --icon-start i --icon-end, więc ikona ustawiona w komponencie nie jest kaskadowo przenoszona do jego elementów podrzędnych.
  • Zapytania o styl kontenera (@container style(...)) wykrywają, kiedy element ma przypisaną ikonę, i renderują ją bez zmian w klasie lub znacznikach.
  • Maski CSS i currentColor renderują ikonę w pseudoelemencie, który jest zgodny z kolorem tekstu elementu. Rozmiar, kolor, odstępy i przesunięcie w pionie są odczytywane z dziedziczonych właściwości --icon-style-* z odpowiednimi wartościami zastępczymi, dzięki czemu można zmienić styl całego poddrzewa z poziomu elementu nadrzędnego. Prefiks --icon-style- zapobiega kolizji tych ustawień z samymi tokenami ikon, które również znajdują się w --icon-*.

Definiowanie tokenów projektowych ikon

Zdefiniuj ikony SVG i wszelkie zastąpienia --icon-style-* jako właściwości niestandardowe wielokrotnego użytku, korzystając z wbudowanych identyfikatorów URI danych SVG lub adresów URL względnych względem katalogu głównego (np. url("/icons/trash.svg")). Gdy ikona jest wczytywana z zewnętrznego pliku SVG, użyj adresu URL względnego względem katalogu głównego lub umieszczonego identyfikatora URI danych. Względny URL używany we właściwości niestandardowej może być rozwiązywany względem arkusza stylów, w którym używana jest ikona, a nie arkusza stylów, w którym zdefiniowana jest właściwość niestandardowa. Jeśli te arkusze stylów znajdują się w różnych katalogach, adres URL może wskazywać niewłaściwy plik.

Aby zachować adres URL względem arkusza stylów, który definiuje token ikony, można użyć zarejestrowanej właściwości: obliczona wartość zarejestrowanej właściwości <url> lub <image> jest określana na podstawie arkusza stylów, który ją zadeklarował. NIE polegaj na rejestrowaniu tokena jako <image> z adresem URL w initial-value. Chrome i Safari rozpoznają adresy URL initial-value w dokumencie, więc działa to tylko w Firefoksie. Chrome rozpoznaje też wartości zadeklarowane w elemencie :root w odniesieniu do dokumentu, chyba że składnia zaczyna się od <url>.

Jeśli nie można uniknąć względnych adresów URL, np. w przypadku arkuszy stylów w różnych katalogach, zarejestruj token ze składnią zaczynającą się od <url> i zadeklaruj rzeczywisty adres URL w :root. W każdej przeglądarce zostanie to rozwiązane w odniesieniu do deklarującego arkusza stylów:

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

Każda ikona znajduje się teraz w 2 miejscach i jest znacznie więcej powtarzalnego kodu, więc warto to zrobić tylko wtedy, gdy projekt ma arkusze stylów w różnych katalogach i łatwą do zarządzania liczbę ikon. Jeśli masz więcej niż kilkanaście ikon, zarejestruj tokeny --icon-* (z wyjątkiem --icon-style-*) ze skryptu za pomocą CSS.registerProperty, używając tej samej składni, zamiast pisać każdy @property ręcznie. Zarejestrowane właściwości są globalne i nie działają w korzeniach shadow, więc mogą nie pasować do każdego projektu. Jeśli możesz użyć adresów URL względnych względem katalogu głównego lub identyfikatorów URI danych, są one prostsze i działają wszędzie.

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

Renderowanie ikon z tokenów

Element wyświetla ikonę, ustawiając --icon-start (na początku) lub --icon-end (na końcu) na jeden z powyższych tokenów. Dwa fragmenty kodu CSS przekształcają to przypisanie w wyrenderowaną ikonę.

Najpierw zarejestruj --icon-start i --icon-end w @property. Składnia "*" akceptuje dowolną wartość, a inherits: false jest ważną częścią: bez niej ikona ustawiona na przycisku byłaby dziedziczona przez każdy element podrzędny i każdy z nich również by ją renderował.

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

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

Po drugie, zapytanie o styl kontenera wykrywa, kiedy właściwość jest ustawiona na elemencie, i renderuje ikonę w pseudoelemencie. Pseudoelement nie dziedziczy właściwości --icon-start (została zarejestrowana jako niedziedziczona), więc pierwsza deklaracja przekazuje wartość bezpośrednio za pomocą inherit. Pozostałe deklaracje dotyczą rozmiaru, maskowania, wyrównania i odstępów. Domyślne wypełnienie to currentColor, więc ikona przyjmuje kolor tekstu elementu i podąża za jego przejściami.

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

Każda deklaracja ma swoje miejsce:

  • vertical-align: middle wyrównuje do połowy wysokości x, co w przypadku większości zestawów ikon jest widocznie nisko. Wyśrodkowanie na 0.5cap utrzymuje środek ikony w stałym miejscu, gdy --icon-style-size rośnie, a niewielka ujemna wartość --icon-style-voffset kompensuje fakt, że oko postrzega przestrzeń nad ikoną jako większą niż przestrzeń pod nią.
  • flex: none: pusty pseudoelement ma minimalny rozmiar równy 0, więc w przycisku flex jest to pierwszy element, który się zmniejsza, gdy etykieta jest długa, potencjalnie do zera. Rozmiar ikony to decyzja projektowa, a nie coś, co powinno być negocjowane przez nacisk na układ.
  • pointer-events: none: zapobiega przechwytywaniu kliknięć przez ikonę i nie pozwala jej stać się event.target.
  • forced-color-adjust: none: w trybie wymuszonych kolorów przeglądarka zastępuje background-color, co powoduje, że zamaskowana ikona znika. Wypełnienie maski jest zachowane, a currentColor nadal ma wymuszony kolor tekstu.
  • content: "" / var(--icon-style-alt, ""): tekst alternatywny dla wygenerowanej treści umożliwia elementowi sterującemu zawierającemu tylko ikonę pobranie nazwy na potrzeby ułatwień dostępu z ikony za pomocą --icon-style-alt (patrz Elementy sterujące zawierające tylko ikonę poniżej). Domyślny pusty tekst alternatywny ukrywa ikony dekoracyjne przed technologią wspomagającą osoby z niepełnosprawnością.

W przypadku ikony końcowej postępuj zgodnie z tym samym wzorcem, używając --icon-end i ::after zamiast --icon-start i ::before. Użyj mask: var(--icon-end) i zastosuj odstępy za pomocą margin-inline-start zamiast margin-inline-end.

Elementy samodzielne i zawierające tylko ikonę

W przypadku samodzielnych ikon lub przycisków zawierających tylko ikonę zastosuj .icon bezpośrednio do pustego elementu (samego przycisku w przypadku elementu sterującego zawierającego tylko ikonę) i ustaw ikonę za pomocą --icon. W tym przypadku --icon jest właściwością wyższego poziomu, a reguła .icon:empty mapuje ją na --icon-start w przypadku ikony na początku:

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

Przykłady użycia

Domyślne ustawienia komponentów w arkuszu stylów

Główną zaletą ikon jako właściwości niestandardowych jest to, że uczestniczą one w kaskadzie. Ustaw ikonę w selektorze, a otrzyma ją każdy pasujący element, bez zmian w znacznikach:

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

Zmiana ikon w zależności od stanu

Ikona jest tylko właściwością, więc zmiana stanu jest zwykłym zastąpieniem CSS. Nie ma drugiego elementu do pokazania lub ukrycia ani klasy do przełączenia:

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

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

To samo dotyczy prezentacji: ustaw --icon-style-size, --icon-style-color lub --icon-style-spacing w kontenerze, np. na pasku narzędzi, a każda ikona w nim zostanie zaktualizowana.

Ikony jednorazowe ze stylami w tekście

Element, którego nie obejmuje żadna reguła komponentu, może ustawić tę samą właściwość w kodzie, na przykład <button style="--icon-start: var(--icon-trash)">. Traktuj to jako ostateczność, a nie domyślne rozwiązanie: gdy ta sama ikona pojawi się więcej niż raz w tym samym typie elementu, przenieś ją do reguły arkusza stylów.

Elementy sterujące tylko z ikonami

Przyciski zawierające tylko ikonę używają klasy .icon i atrybutu --icon opisanych powyżej. Nadaj przyciskowi nazwę na potrzeby ułatwień dostępu za pomocą aria-label lub pozwól, aby ikona dostarczyła ją za pomocą --icon-style-alt. Bez żadnego z tych elementów ikona wygenerowana przez CSS jest elementem dekoracyjnym. W przypadku samodzielnych ikon informacyjnych udostępnij element .icon z atrybutami role="img" i 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>

Bezpośrednia praca z plikami SVG

Jeśli system ikon CSS nie jest odpowiedni, użyj wbudowanego kodu SVG i elementu <use> razem, a nie jako alternatywy. Wstaw kod SVG, gdy ikona pojawia się na stronie po raz pierwszy, i odwołuj się do niego za pomocą <use href="#id"> przy każdym powtórzeniu. Odwołanie lokalne nie wymaga żądania i nie podlega ograniczeniom współdzielenia. Zarezerwuj zewnętrzny arkusz sprite (<use href="/icons.svg#id">) na ikony udostępniane na wielu stronach.

W przypadku elementu <use> treści, do których się odwołuje, są klonowane do drzewa shadow, więc arkusz CSS strony nie może wybrać jego elementów. Dziedziczone są: color, currentColor i wszystkie właściwości niestandardowe. Oprócz atrybutów prezentacji SVG nowoczesny CSS może też zmieniać wiele atrybutów geometrii, dzięki czemu ikona, którą kontrolujesz, może udostępniać dokładnie te elementy, których potrzebujesz, na przykład stroke-width="var(--icon-stroke, 2)", a nawet d: var(--icon-path, path(…)) na określonej ścieżce.

Właściwość CSS d nie jest obsługiwana w Safari, więc używaj tego dostosowania tylko wtedy, gdy brak obsługi jest akceptowalny. Jeśli nie masz kontroli nad ikoną, umieść ją w kodzie.

<use> pułapki, o których warto wiedzieć:

  • Atrybut viewBox musi znajdować się w elemencie <symbol> lub w elemencie, do którego odwołuje się <svg>. Zewnętrzny element <svg>, który zawiera element <use>, nie pobiera go z odwołania, dlatego ustaw atrybuty width i height w elemencie nadrzędnym i zachowaj atrybut viewBox z symbolem.
  • Style nie przekraczają granicy w żadnym kierunku: <style> wewnątrz zewnętrznego duszka nie ma zastosowania do strony, a arkusze stylów strony nie mają zastosowania wewnątrz drzewa, do którego się odwołują. Przekazywane są tylko właściwości dziedziczone i właściwości niestandardowe.
  • Odwołania do elementów zewnętrznych są możliwe tylko w ramach tej samej domeny. W przypadku współdzielenia zasobów między domenami (CORS) duszki nie działają, ale nie jest wyświetlany żaden komunikat o błędzie.
  • Duszki wstawione do dokumentu współdzielą przestrzeń nazw id strony, więc identyfikatory symboli przestrzeni nazw pozwalają uniknąć kolizji.

Oznacz dekoracyjne elementy SVG za pomocą aria-hidden="true" i nadaj elementowi sterującemu zawierającemu te elementy własną nazwę na potrzeby ułatwień dostępu, tak jak w przypadku systemu ikon CSS.

Strategie zastępcze

Zarejestrowane właściwości niestandardowe

Bez @property --icon-start i --icon-end zachowują się jak zwykłe dziedziczące właściwości niestandardowe, więc ikona ustawiona na elemencie będzie też renderowana na jego elementach podrzędnych. Aby temu zapobiec, zresetuj właściwości elementów podrzędnych w zapytaniu o styl (@container style(--icon-start) { * { --icon-start: initial; } }). To samo podejście jest stosowane w poniższym obejściu problemu w Safari.

Zapytania o styl kontenera

Aby obsługiwać przeglądarki bez zapytań o styl kontenera, użyj klas lub selektorów komponentów jako elementów zastępczych. Zachowaj wybór ikon we właściwościach niestandardowych. Selektory stylu wbudowanego nie uwzględniają ikon przypisanych za pomocą reguł 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);
}

W przypadku ikon na końcu użyj tego samego zastępczego elementu class-hook z .icon-end, --icon-end i ::after zamiast .icon-start, --icon-start i ::before. Zastosuj odstępy do niepustych treści za pomocą margin-inline-start zamiast margin-inline-end.

Ten wzorzec image-set() sprawia, że content jest nieprawidłowy, gdy właściwość niestandardowa nie jest ustawiona, więc wraca do none, a pseudoelement znika bez dodawania ani usuwania klasy. Nie usuwa innych stylów pseudoelementu ani nie zapobiega konfliktom z innymi regułami ::before lub ::after, dlatego używaj tych punktów zaczepienia tylko tam, gdzie te pseudoelementy są dostępne dla ikon.

Obejścia problemów z przeglądarką Safari

Safari ma 2 błędy renderowania, które wpływają na ikony: WebKit #301609 może uniemożliwiać ich wyświetlanie, dopóki użytkownik nie wejdzie z nimi w interakcję, a #320220 również wpływa na ich renderowanie. Poniższe rozwiązanie dotyczy obu tych problemów:

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

Zapytanie @supports jest kierowane tylko na Safari, a @layer ogranicza konflikty animacji.

Błąd WebKit nr 325900 może w niektórych przypadkach uniemożliwić bezpośrednie zastąpienie elementu --icon-start, np. zmianę ikony po najechaniu kursorem. Ma to wpływ na obejście problemu w przypadku zarejestrowanych właściwości, które nie są dziedziczone. W przypadku Safari zarejestruj właściwość --icon-start jako dziedziczoną i zresetuj ją w elementach podrzędnych wewnątrz zapytania o styl:

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

Resetowanie zapobiega propagowaniu wartości ikony do elementów podrzędnych. Umieszczenie go w warstwie sprawia, że deklaracje ikon bez warstw mają pierwszeństwo przed resetowaniem.