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-starti--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
currentColorrenderują 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:middlewyrównuje do połowy wysokości x, co w przypadku większości zestawów ikon jest widocznie nisko. Wyśrodkowanie na0.5caputrzymuje środek ikony w stałym miejscu, gdy--icon-style-sizerośnie, a niewielka ujemna wartość--icon-style-voffsetkompensuje 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ępujebackground-color, co powoduje, że zamaskowana ikona znika. Wypełnienie maski jest zachowane, acurrentColornadal 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
viewBoxmusi 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 atrybutywidthiheightw elemencie nadrzędnym i zachowaj atrybutviewBoxz 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
idstrony, 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.