Como escolher uma técnica de ícone
O armazenamento e a renderização de ícones são escolhas separadas. Por exemplo, um sprite SVG pode fornecer um ícone para o sistema de ícones CSS ou ser referenciado com <use>.
- Sistema de ícones CSS: use quando o CSS precisar escolher quando e qual ícone mostrar. Use uma máscara para um ícone de cor única ou uma imagem de plano de fundo para um ícone colorido.
- SVG com
<use>: use para reutilizar um símbolo quando as opções de estilo integradas forem suficientes. O CSS não pode estilizar ou animar arbitrariamente o conteúdo do SVG referenciado, mas as propriedades herdadas e personalizadas podem ser transmitidas quando o símbolo é projetado para usá-las. Por exemplo, um caminho pode usarfill="var(--icon-accent, currentColor)"para permitir que o CSS pinte essa parte do ícone. - SVG inline: use quando precisar de estilo ou animação arbitrários do conteúdo do SVG.
<img>com filtros CSS: evite usar essa opção para mudar a cor dos ícones. Os filtros não podem aplicar de forma confiável um valor de cor CSS arbitrário à imagem, então a recolorização é complicada e inflexível.
Para decidir, faça duas perguntas. O CSS precisa escolher ou trocar o ícone (padrões de componente, mudanças de estado, temas)? Se sim, use o sistema de ícones do CSS. Você precisa acessar o ícone além da cor? Se sim e você controla a origem do ícone, projete o símbolo com propriedades personalizadas e use <use>. Se você não controla a origem, use o SVG inline. Os conjuntos de ícones de terceiros raramente expõem propriedades personalizadas, então o <use> contra eles oferece cor e pouco mais.
Ícones incorporados ou remotos
Tanto o sistema de ícones CSS quanto o <use> podem usar ícones de um URI de dados, um arquivo individual ou um sprite. A incorporação (URIs de dados ou SVG inline) evita uma solicitação e renderiza com a primeira pintura, mas aumenta o CSS ou HTML e não pode ser armazenada em cache separadamente. Por isso, é adequada para um pequeno conjunto de ícones que aparecem em todas as páginas. Os arquivos individuais são armazenados em cache de forma independente e carregados sob demanda, ao custo de uma solicitação cada. Um sprite é uma solicitação que pode ser armazenada em cache para todo o conjunto, mas faz o download de todos os ícones, sejam usados ou não, e o <use> só pode referenciar um sprite da mesma origem. Prefira incorporar alguns ícones principais e um sprite ou arquivos individuais para conjuntos maiores.
Se uma página renderizar milhares de ícones de uma só vez, faça um comparativo de mercado da página e dos recursos reais. Em um teste de estresse, o SVG inline teve uma pequena vantagem de desempenho nessa escala, mas os resultados dependem do navegador e da implementação. Teste de estresse de ícones SVG do Cloud Four (em inglês)
Abordagem principal: mecanismo de ícones orientado por CSS
Os ícones são tokens de design: cada ícone é uma propriedade personalizada que contém uma imagem, e qualquer elemento pode mostrar um ao definir --icon-start ou --icon-end. O mecanismo combina três recursos de CSS:
- As propriedades personalizadas registradas (
@property) impedem que--icon-starte--icon-endsejam herdados. Assim, um ícone definido em um componente não é transmitido para os filhos. - As consultas de estilo de contêiner (
@container style(...)) detectam quando um elemento tem um ícone atribuído e o renderizam, sem mudanças de classe ou marcação. - As máscaras CSS e
currentColorrenderizam o ícone em um pseudoelemento que segue a cor do texto do elemento. O tamanho, a cor, o espaçamento e o deslocamento vertical são lidos de propriedades--icon-style-*herdáveis com substitutos adequados, de modo que uma subárvore inteira possa ser reestilizada de um ancestral. O prefixo--icon-style-impede que essas configurações entrem em conflito com os próprios tokens de ícone, que também estão em--icon-*.
Definição de tokens de design de ícones
Defina ícones SVG e todas as substituições de --icon-style-* como propriedades personalizadas reutilizáveis usando URIs de dados SVG inline ou URLs relativos à raiz (por exemplo, url("/icons/trash.svg")). Quando um ícone for carregado de um arquivo SVG externo, use um URL relativo à raiz ou um URI de dados incorporado. Um URL relativo usado por uma propriedade personalizada pode ser resolvido em relação à folha de estilo em que o ícone é usado, em vez da folha de estilo em que a propriedade personalizada é definida. Se essas folhas de estilo estiverem em diretórios diferentes, o URL poderá ser resolvido para o arquivo errado.
Para manter um URL relativo à folha de estilo que define o token do ícone, uma propriedade registrada pode ajudar: o valor calculado de um <url> ou <image> registrado é resolvido em relação à folha de estilo que o declarou. NÃO registre o token como <image> com o URL em initial-value. O Chrome e o Safari resolvem URLs initial-value em relação ao documento, então isso só funciona no Firefox. O Chrome também resolve valores declarados em :root em relação ao documento, a menos que a sintaxe comece com <url>.
Quando os URLs relativos são inevitáveis, como folhas de estilo em diretórios diferentes, registre o token com uma sintaxe que comece com <url> e declare o URL real em :root. Isso é resolvido em relação à folha de estilo de declaração em todos os navegadores:
@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");
}
Cada ícone agora fica em dois lugares, e há muito mais código boilerplate. Por isso, essa abordagem só vale a pena quando um projeto tem folhas de estilo em diretórios diferentes e um número gerenciável de ícones. Depois de uma dúzia de ícones, registre os tokens --icon-* (exceto --icon-style-*) de um script com CSS.registerProperty, usando a mesma sintaxe, em vez de escrever cada @property manualmente. As propriedades registradas são globais e não funcionam em raízes de sombra. Portanto, isso pode não se adequar a todos os projetos. Se os URLs relativos à raiz ou os URIs de dados forem uma opção, eles são mais simples e funcionam em qualquer lugar.
: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");
}
Renderização de ícones com base em tokens
Um elemento mostra um ícone ao definir --icon-start (à esquerda) ou --icon-end (à direita) como um dos tokens acima. Duas partes de CSS transformam essa atribuição em um ícone renderizado.
Primeiro, registre --icon-start e --icon-end com @property. A sintaxe "*" aceita qualquer valor, e inherits: false é a parte importante: sem ela, um ícone definido em um botão seria herdado por todos os descendentes, e cada um deles também renderizaria o ícone.
@property --icon-start {
syntax: "*";
inherits: false;
}
@property --icon-end {
syntax: "*";
inherits: false;
}
Em segundo lugar, uma consulta de estilo de contêiner detecta quando a propriedade é definida em um elemento e renderiza o ícone em um pseudoelemento. Como o pseudoelemento não herda --icon-start (ele foi registrado como não herdado), a primeira declaração encaminha o valor explicitamente com inherit. As declarações restantes lidam com dimensionamento, mascaramento, alinhamento e espaçamento. Como o preenchimento padrão é currentColor, o ícone adota a cor do texto do elemento e segue as transições dele.
/* 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);
}
}
Cada declaração tem seu lugar:
vertical-align:middlese alinha à metade da altura x, que fica visivelmente baixa para a maioria dos conjuntos de ícones. A centralização em0.5capmantém o centro do ícone fixo enquanto--icon-style-sizeaumenta, e o pequeno valor negativo de--icon-style-voffsetcompensa o fato de que o olho humano percebe o espaço acima de um ícone como maior do que o espaço abaixo dele.flex: none: um pseudoelemento vazio tem um tamanho mínimo de zero. Portanto, dentro de um botão flexível, ele é o primeiro a encolher quando o rótulo é longo, podendo até desaparecer. O tamanho do ícone é uma decisão de design, não algo que a pressão do layout precisa negociar.pointer-events: none: impede que o ícone capture cliques ou se torne oevent.target.forced-color-adjust: none: no modo de cores forçadas, o navegador substituibackground-color, fazendo com que um ícone mascarado desapareça. O preenchimento da máscara é preservado, ecurrentColorainda segue a cor de texto forçada.content: "" / var(--icon-style-alt, ""): o texto alternativo para conteúdo gerado permite que um controle somente de ícone use o nome acessível do ícone por meio de--icon-style-alt(consulte "Controles somente de ícone" abaixo). O atributo alt vazio padrão mantém os ícones decorativos ocultos da tecnologia adaptativa.
Para um ícone final, siga o mesmo padrão com --icon-end e ::after em vez de --icon-start e ::before. Use mask: var(--icon-end) e aplique o espaçamento com margin-inline-start em vez de margin-inline-end.
Elementos independentes e somente com ícones
Para ícones independentes ou botões que só têm ícones, aplique .icon diretamente ao elemento vazio (o próprio botão para um controle que só tem ícones) e defina o ícone com --icon. Aqui, --icon é a propriedade de nível superior. A regra .icon:empty a mapeia para --icon-start para um ícone principal:
.icon:empty {
--icon-start: var(--icon);
}
Exemplos de uso
Padrões de componentes na folha de estilo
O principal benefício dos ícones como propriedades personalizadas é que eles participam da cascata. Defina um ícone em um seletor, e todos os elementos correspondentes vão recebê-lo, sem mudanças na marcação:
.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>
Como mudar ícones com o estado
Como o ícone é apenas uma propriedade, uma mudança de estado é uma substituição comum de CSS. Não há um segundo elemento para mostrar ou ocultar, nem uma classe para alternar:
.button-expand {
--icon-start: var(--icon-caret-right);
}
.button-expand[aria-expanded="true"] {
--icon-start: var(--icon-caret-down);
}
O mesmo se aplica à apresentação: defina --icon-style-size, --icon-style-color ou --icon-style-spacing em um contêiner, como uma barra de ferramentas, e todos os ícones dentro dele serão atualizados.
Ícones únicos com estilos inline
Um elemento que não é coberto por nenhuma regra de componente pode definir a mesma propriedade inline, por exemplo, <button style="--icon-start: var(--icon-trash)">. Trate isso como uma saída de emergência, não como o padrão: quando o mesmo ícone aparecer no mesmo tipo de elemento mais de uma vez, mova-o para uma regra de folha de estilo.
Controles somente com ícones
Os botões somente com ícone usam a classe .icon e o --icon descritos acima. Dê ao botão um nome acessível com aria-label ou deixe que o ícone forneça um com --icon-style-alt. Sem nenhum dos dois, o ícone gerado pelo CSS é decorativo. Para ícones informativos independentes, exponha o elemento .icon com 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>
Como trabalhar diretamente com SVG
Quando o sistema de ícones CSS não for adequado, use o SVG inline e o <use> juntos, em vez de como alternativas. Insira o SVG inline na primeira vez que um ícone aparecer em uma página e faça referência a ele com <use href="#id"> para cada repetição. A referência local não custa nenhuma solicitação e não tem restrição entre origens. Reserve um sprite externo (<use href="/icons.svg#id">) para ícones compartilhados em várias páginas.
Com <use>, o conteúdo referenciado é clonado em uma árvore de sombra, de modo que o CSS da página não pode selecionar os elementos dele. O que é alcançado é a herança: color, currentColor e qualquer propriedade personalizada. Além dos atributos de apresentação de SVG, o CSS moderno também pode mudar muitos atributos de geometria. Assim, um ícone que você controla pode expor exatamente os ganchos necessários, por exemplo, stroke-width="var(--icon-stroke, 2)" ou até mesmo d: var(--icon-path, path(…)) em um caminho específico.
O CSS d não é compatível com o Safari. Portanto, use essa personalização apenas quando essa lacuna de suporte for aceitável. Se você não controla o ícone, use-o inline.
<use> pegadinhas que você precisa conhecer:
- O
viewBoxprecisa estar no<symbol>ou no<svg>referenciado. O<svg>externo que envolve o<use>não o pega da referência. Portanto, definawidtheheightno wrapper e mantenha oviewBoxcom o símbolo. - Os estilos não cruzam o limite em nenhuma direção:
<style>dentro de um sprite externo não se aplica à página, e as folhas de estilo da página não se aplicam dentro da árvore referenciada. Somente as propriedades herdadas e personalizadas são transmitidas. - As referências externas são apenas de mesma origem. Os sprites entre origens falham silenciosamente.
- Os sprites inline no documento compartilham o namespace
idda página. Portanto, use IDs de símbolo de namespace para evitar colisões.
Marque SVGs decorativos com aria-hidden="true" e dê ao controle que os contém um nome acessível próprio, como no sistema de ícones CSS.
Estratégias de fallback
Propriedades personalizadas registradas
Sem @property, --icon-start e --icon-end se comportam como propriedades personalizadas de herança comuns. Assim, um ícone definido em um elemento também é renderizado nos descendentes. Para evitar isso, redefina as propriedades nos filhos dentro da consulta de estilo (@container style(--icon-start) { * { --icon-start: initial; } }), a mesma abordagem usada na solução alternativa do Safari abaixo.
Consultas de estilo de contêiner
Para oferecer suporte a navegadores sem consultas de estilo de contêiner, use classes ou seletores de componentes como hooks de substituição. Mantenha a seleção de ícones nas propriedades personalizadas. Os seletores de estilo inline não incluem ícones atribuídos por regras de 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);
}
Para ícones finais, use o mesmo substituto de hook de classe com .icon-end, --icon-end e ::after em vez de .icon-start, --icon-start e ::before. Aplique espaçamento ao conteúdo não vazio com margin-inline-start em vez de margin-inline-end.
Esse padrão image-set() torna content inválido quando a propriedade personalizada não está definida. Assim, ele volta para none, e o pseudoelemento desaparece sem adicionar ou remover uma classe. Ele não remove os outros estilos do pseudoelemento nem evita conflitos com outras regras ::before ou ::after. Portanto, use esses hooks apenas onde esses pseudoelementos estiverem disponíveis para ícones.
Soluções alternativas para o navegador Safari
O Safari tem dois bugs de renderização que afetam os ícones: o WebKit #301609 pode impedir que eles apareçam até que o usuário interaja, e o #320220 também afeta a renderização. A solução alternativa abaixo resolve os dois problemas:
@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;
}
}
}
A consulta @supports segmenta apenas o Safari, e @layer reduz os conflitos de animação.
O bug #325900 do WebKit pode impedir que uma substituição direta de --icon-start entre em vigor em alguns casos, como ao mudar o ícone ao passar o cursor. Isso afeta a solução alternativa para propriedades registradas que não são herdadas. Para o Safari, registre --icon-start como herdado e redefina-o em descendentes dentro da consulta de estilo:
@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;
}
}
}
}
A redefinição impede que o valor do ícone seja propagado para os descendentes. Manter o ícone em uma camada permite que as declarações de ícones sem camada tenham precedência sobre a redefinição.