Cómo usar y administrar íconos

Cómo elegir una técnica de íconos

El almacenamiento y la renderización de íconos son opciones independientes. Por ejemplo, un sprite de SVG puede proporcionar un ícono al sistema de íconos de CSS o se puede hacer referencia a él con <use>.

  • Sistema de íconos de CSS: Se usa cuando CSS debe elegir cuándo y qué ícono mostrar. Usa una máscara para un ícono de un solo color o una imagen de fondo para un ícono a todo color.
  • SVG con <use>: Se usa para reutilizar un símbolo cuando sus opciones de diseño integradas son suficientes. CSS no puede aplicar un estilo ni animar de forma arbitraria el contenido del SVG al que se hace referencia, pero las propiedades heredadas y las propiedades personalizadas se pueden pasar cuando el símbolo está diseñado para usarlas. Por ejemplo, una ruta de acceso puede usar fill="var(--icon-accent, currentColor)" para permitir que CSS coloree esa parte del ícono.
  • SVG intercalado: Úsalo cuando necesites aplicar un estilo o una animación arbitrarios al contenido del SVG.
  • <img> con filtros de CSS: Evita usar este método para cambiar el color de los íconos. Los filtros no pueden aplicar de forma confiable un valor de color CSS arbitrario a la imagen, por lo que volver a colorear es engorroso y poco flexible.

Para decidir, hazte dos preguntas. ¿El CSS necesita elegir o intercambiar el ícono (valores predeterminados de los componentes, cambios de estado, temas)? Si es así, usa el sistema de íconos de CSS. ¿Necesitas acceder al interior del ícono más allá del color? Si es así y controlas la fuente del ícono, diseña el símbolo con propiedades personalizadas y usa <use>. Si no controlas la fuente, usa el SVG intercalado. Los conjuntos de íconos de terceros rara vez exponen propiedades personalizadas, por lo que <use> contra ellos te brinda color y poco más.

Íconos incorporados o remotos

Tanto el sistema de íconos de CSS como <use> pueden tomar íconos de un URI de datos, un archivo individual o un objeto de juego. La incorporación (URI de datos o SVG intercalado) evita una solicitud y se renderiza con el primer procesamiento de imagen, pero aumenta el tamaño del CSS o HTML y no se puede almacenar en caché por separado, por lo que es adecuada para un conjunto pequeño de íconos que aparecen en todas las páginas. Los archivos individuales se almacenan en caché de forma independiente y se cargan a pedido, con el costo de una solicitud cada uno. Un sprite es una solicitud almacenable en caché para todo el conjunto, pero descarga todos los íconos, se usen o no, y <use> solo puede hacer referencia a un sprite del mismo origen. Se recomienda incorporar un puñado de íconos principales y un sprite o archivos individuales para conjuntos más grandes.

Si una página renderiza miles de íconos a la vez, compara la página y los recursos reales. En una prueba de esfuerzo, el SVG intercalado tuvo una pequeña ventaja de rendimiento en esa escala, pero los resultados dependen del navegador y de la implementación. Prueba de esfuerzo de íconos SVG de Cloud Four

Enfoque principal: Motor de íconos basado en CSS

Los íconos son tokens de diseño: cada ícono es una propiedad personalizada que contiene una imagen, y cualquier elemento puede mostrar uno si se configura --icon-start o --icon-end. El motor combina tres capacidades de CSS:

  • Las propiedades personalizadas registradas (@property) evitan que se hereden --icon-start y --icon-end, por lo que un ícono establecido en un componente no se propaga en cascada a sus elementos secundarios.
  • Las consultas de estilo de contenedor (@container style(...)) detectan cuándo un elemento tiene un ícono asignado y lo renderizan, sin cambios de clase ni de lenguaje de marcado.
  • Las máscaras CSS y currentColor renderizan el ícono en un seudoelemento que sigue el color del texto del elemento. El tamaño, el color, el espaciado y el desplazamiento vertical se leen de las propiedades heredables de --icon-style-* con resguardos razonables, por lo que se puede cambiar el estilo de todo un subárbol desde un ancestro. El prefijo --icon-style- evita que estos parámetros de configuración entren en conflicto con los tokens de íconos, que también se encuentran en --icon-*.

Cómo definir tokens de diseño de íconos

Define los íconos de SVG y cualquier anulación de --icon-style-* como propiedades personalizadas reutilizables con URIs de datos de SVG intercalados o URLs relativas a la raíz (p.ej., url("/icons/trash.svg")). Cuando se cargue un ícono desde un archivo SVG externo, usa una URL relativa a la raíz o un URI de datos incorporado. Una URL relativa que se usa a través de una propiedad personalizada puede resolverse en relación con la hoja de estilo en la que se usa el ícono, en lugar de la hoja de estilo en la que se define la propiedad personalizada. Si esas hojas de estilo están en directorios diferentes, es posible que la URL se resuelva en el archivo incorrecto.

Para mantener una URL relativa a la hoja de estilo que define el token de ícono, se diseñó una propiedad registrada para ayudar: el valor calculado de un <url> o <image> registrado se resuelve en relación con la hoja de estilo que lo declaró. NO confíes en registrar el token como <image> con la URL en initial-value. Chrome y Safari resuelven las URLs de initial-value en relación con el documento, por lo que solo funciona en Firefox. Chrome también resuelve los valores declarados en :root con respecto al documento, a menos que la sintaxis comience con <url>.

Cuando las URLs relativas son inevitables, como las hojas de estilo en diferentes directorios, registra el token con una sintaxis que comience con <url> y declara la URL real en :root. Esto se resuelve con la hoja de estilo de declaración en todos los 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 ícono ahora se encuentra en dos lugares y hay mucho más código estándar, por lo que esto solo vale la pena cuando un proyecto tiene hojas de estilo en diferentes directorios y una cantidad manejable de íconos. Si tienes más de una docena de íconos, registra los tokens de --icon-* (excepto --icon-style-*) desde una secuencia de comandos con CSS.registerProperty, usando la misma sintaxis, en lugar de escribir cada @property a mano. Las propiedades registradas son globales y no funcionan en raíces de sombra, por lo que es posible que esto no se ajuste a todos los proyectos. Si las URLs relativas a la raíz o los URIs de datos son una opción, son más simples y funcionan en todas partes.

: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 íconos a partir de tokens

Un elemento muestra un ícono configurando --icon-start (inicial) o --icon-end (final) en uno de los tokens anteriores. Dos fragmentos de CSS convierten esa asignación en un ícono renderizado.

Primero, registra --icon-start y --icon-end con @property. La sintaxis "*" acepta cualquier valor, y inherits: false es la parte importante: sin ella, un ícono establecido en un botón se heredaría en cada descendiente, y cada uno de ellos también renderizaría el ícono.

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

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

En segundo lugar, una consulta de estilo de contenedor detecta cuándo se establece la propiedad en un elemento y renderiza el ícono en un seudoelemento. Debido a que el pseudoelemento no hereda --icon-start (se registró como no heredado), la primera declaración reenvía el valor de forma explícita con inherit. Las declaraciones restantes controlan el tamaño, el enmascaramiento, la alineación y el espaciado. Como el relleno predeterminado es currentColor, el ícono adopta el color del texto del elemento y sigue sus transiciones.

/* 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 declaración tiene su lugar:

  • vertical-align: middle se alinea con la mitad de la altura de la x, que se ubica visiblemente baja para la mayoría de los conjuntos de íconos. Centrar en 0.5cap mantiene fijo el centro del ícono a medida que crece --icon-style-size, y el pequeño valor negativo de --icon-style-voffset compensa el hecho de que el ojo pondera más el espacio que está por encima de un ícono que el que está por debajo.
  • flex: none: Un pseudoelemento vacío tiene un tamaño mínimo de cero, por lo que, dentro de un botón flexible, es lo primero que se reduce cuando la etiqueta es larga, posiblemente a nada. El tamaño del ícono es una decisión de diseño, no algo que deba negociar la presión del diseño.
  • pointer-events: none: Evita que el ícono capture clics o se convierta en el event.target.
  • forced-color-adjust: none: En el modo de colores forzados, el navegador reemplaza background-color, lo que hace que desaparezca un ícono enmascarado. Se conserva el relleno de la máscara y currentColor sigue el color de texto forzado.
  • content: "" / var(--icon-style-alt, ""): El texto alternativo para el contenido generado permite que un control que solo tiene un ícono tome su nombre de accesibilidad del ícono a través de --icon-style-alt (consulta Controles que solo tienen un ícono a continuación). El atributo alt vacío predeterminado mantiene ocultos los íconos decorativos de la tecnología de accesibilidad.

Para un ícono final, sigue el mismo patrón con --icon-end y ::after en lugar de --icon-start y ::before. Usa mask: var(--icon-end) y aplica espaciado con margin-inline-start en lugar de margin-inline-end.

Elementos independientes y solo de íconos

Para los íconos independientes o los botones que solo contienen íconos, aplica .icon directamente al elemento vacío (el botón en sí para un control que solo contiene íconos) y establece su ícono con --icon. Aquí, --icon es la propiedad de nivel superior; la regla .icon:empty la asigna a --icon-start para un ícono inicial:

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

Ejemplos de uso

Valores predeterminados de los componentes en la hoja de estilo

El principal beneficio de los íconos como propiedades personalizadas es que participan en la cascada. Establece un ícono en un selector y todos los elementos que coincidan lo obtendrán, sin cambios en el lenguaje de marcado:

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

Cómo cambiar íconos con el estado

Como el ícono es solo una propiedad, un cambio de estado es una anulación de CSS común. No hay un segundo elemento para mostrar u ocultar, ni una clase para activar o desactivar:

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

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

Lo mismo se aplica a la presentación: establece --icon-style-size, --icon-style-color o --icon-style-spacing en un contenedor, como una barra de herramientas, y se actualizarán todos los íconos que contenga.

Íconos únicos con estilos intercalados

Un elemento que no esté cubierto por ninguna regla de componente puede establecer la misma propiedad intercalada, por ejemplo, <button style="--icon-start: var(--icon-trash)">. Trata esto como una vía de escape más que como la opción predeterminada: una vez que el mismo ícono aparezca en el mismo tipo de elemento más de una vez, muévelo a una regla de hoja de estilo.

Controles solo con íconos

Los botones solo de ícono usan la clase .icon y el --icon que se describieron anteriormente. Asigna un nombre de accesibilidad al botón con aria-label o permite que el ícono proporcione uno a través de --icon-style-alt. Sin ninguno de los dos, el ícono generado por CSS es decorativo. Para los íconos informativos independientes, expón el elemento .icon con role="img" y 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>

Cómo trabajar directamente con SVG

Cuando el sistema de íconos CSS no sea el adecuado, usa SVG intercalado y <use> juntos, en lugar de como alternativas. Intercala el SVG la primera vez que aparezca un ícono en una página y haz referencia a él con <use href="#id"> para cada repetición. La referencia local no cuesta ninguna solicitud y no tiene restricciones de origen cruzado. Reserva un sprite externo (<use href="/icons.svg#id">) para los íconos que se comparten en muchas páginas.

Con <use>, el contenido al que se hace referencia se clona en un árbol de sombra, por lo que el CSS de la página no puede seleccionar sus elementos. Lo que sí se hereda es lo siguiente: color, currentColor y cualquier propiedad personalizada. Más allá de los atributos de presentación de SVG, el CSS moderno también puede cambiar muchos atributos de geometría, por lo que un ícono que controles puede exponer exactamente los hooks que necesitas, por ejemplo, stroke-width="var(--icon-stroke, 2)" o incluso d: var(--icon-path, path(…)) en una ruta de acceso en particular.

Safari no admite CSS d, por lo que debes usar esta personalización solo cuando esa brecha de compatibilidad sea aceptable. Si no controlas el ícono, inclúyelo de forma intercalada.

<use> problemas que vale la pena conocer:

  • El viewBox debe estar en el <symbol> o en el <svg> al que se hace referencia; el <svg> externo que une <use> no lo toma de la referencia, por lo que debes establecer width y height en el wrapper y mantener el viewBox con el símbolo.
  • Los estilos no cruzan el límite en ninguna dirección: <style> dentro de un sprite externo no se aplica a la página, y las hojas de estilo de la página no se aplican dentro del árbol al que se hace referencia. Solo se pasan las propiedades heredadas y las personalizadas.
  • Las referencias externas solo son del mismo origen; los sprites de origen cruzado fallan de forma silenciosa.
  • Los sprites intercalados en el documento comparten el espacio de nombres id de la página, por lo que los IDs de símbolos de espacio de nombres evitan colisiones.

Marca los SVG decorativos con aria-hidden="true" y asigna al control que los contiene su propio nombre de accesibilidad, como con el sistema de íconos de CSS.

Estrategias de resguardo

Propiedades personalizadas registradas

Sin @property, --icon-start y --icon-end se comportan como propiedades personalizadas heredadas comunes, por lo que un ícono establecido en un elemento también se renderizaría en sus descendientes. Para evitarlo, restablece las propiedades de los elementos secundarios dentro de la consulta de estilo (@container style(--icon-start) { * { --icon-start: initial; } }), el mismo enfoque que usa la solución alternativa de Safari que se muestra a continuación.

Consultas de estilo de contenedor

Para admitir navegadores sin consultas de estilo de contenedor, usa clases o selectores de componentes como hooks de resguardo. Mantén la selección de íconos en las propiedades personalizadas. Los selectores de estilo intercalado por sí solos no incluyen los íconos asignados a través de reglas 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 los íconos finales, usa el mismo resguardo de hook de clase con .icon-end, --icon-end y ::after en lugar de .icon-start, --icon-start y ::before. Aplica espaciado al contenido no vacío con margin-inline-start en lugar de margin-inline-end.

Este patrón image-set() hace que content no sea válido cuando no se establece la propiedad personalizada, por lo que recurre a none y el seudoelemento desaparece sin agregar ni quitar una clase. No quita los otros estilos del pseudoelemento ni evita conflictos con otras reglas ::before o ::after, por lo que debes usar estos hooks solo cuando esos pseudoelementos estén disponibles para los íconos.

Soluciones alternativas para el navegador Safari

Safari tiene dos errores de renderización que afectan a los íconos: WebKit #301609 puede impedir que aparezcan hasta que el usuario interactúe con ellos, y #320220 también afecta su renderización. La siguiente solución alternativa aborda ambos 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;
    }
  }
}

La consulta @supports se orienta solo a Safari, y @layer reduce los conflictos de animación.

El error n° 325900 de WebKit puede impedir que se aplique una anulación directa de --icon-start en algunos casos, como cuando se cambia el ícono al colocar el cursor sobre él. Esto afecta la solución alternativa para las propiedades registradas que no se heredan. Para Safari, registra --icon-start como heredado y restablece su valor en los descendientes dentro de la 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;
      }
    }
  }
}

El restablecimiento evita que el valor del ícono se propague a los descendientes. Mantenerlo en una capa permite que las declaraciones de íconos sin capa tengan prioridad sobre el restablecimiento.