Skip to content

Icon

A glyph, drawn from an SVG path. Reach for it for the small symbols an interface is made of: a tick, a chevron, a bell, the thing on the left of a menu row. For a photograph or a thumbnail use an image, which decodes a file rather than drawing a path.

An Icon takes path data, not a file. There is no SVG parser in the framework and nothing to load: the path is rasterised into a small bitmap, and the bitmap is drawn into a box exactly as a picture is. Icon sets ship their paths as strings, so pasting one in is the whole of adopting a set.

tsx
/**
 * Seven icons: four stroked, two filled, and one nobody announces.
 *
 * Every one of them names a palette entry rather than a colour, which
 * is the same rule the rest of the library follows, and it costs more
 * to keep here than anywhere else: a raster has its colour baked into
 * its pixels, so a palette name cannot be resolved at paint. The
 * modifier resolves it against the theme the node inherits and
 * rasterises again when that theme changes. Switch this site between
 * light and dark and the icons below are redrawn, in the accent of
 * whichever palette arrived.
 *
 * `Search` and `Alerts` are two subpaths in one string. An icon is one
 * path in one colour by construction, so a two-colour mark is two
 * `Icon`s rather than one.
 */
export function Glyphs(_inputs: Inputs<{}>, _ctx: ComponentContext) {
  return (
    <column gap={16} padding={20} width={percent(100)} height={percent(100)}>
      <row gap={16} y="center">
        {STROKED.map(icon => (
          <Icon
            key={icon.label}
            path={icon.path}
            label={icon.label}
            size={24}
            style="stroke"
            strokeWidth={2}
            color="controlAccent"
          />
        ))}
        <text text="stroke, at strokeWidth 2" fontSize={12} color="textMuted" />
      </row>
      <row gap={16} y="center">
        <Icon path={PLAY} label="Play" size={24} color="text" />
        <Icon path={CLOCK} label="Recent" size={24} color="text" fillRule="evenodd" />
        <text text="fill, and fill with evenodd so the clock keeps its face" fontSize={12} color="textMuted" />
      </row>
      <row gap={16} y="center">
        <Icon path={PLAY} size={12} color="textMuted" />
        <text text="No label, so this one is decorative and is not announced." fontSize={12} color="textMuted" />
      </row>
    </column>
  );
}

Switch this site between light and dark, and watch the accent of the stroked four follow it. That is not a repaint: each icon is drawn again.

The paths themselves are ordinary strings, kept beside the component that uses them:

tsx
/**
 * The paths, on the 24-unit grid every icon set uses.
 *
 * An `Icon` takes path data rather than a file, because what it does
 * with it is rasterise it: there is no document to load and no SVG
 * parser in the framework. The stroked four are drawn here; the
 * filled two are Heroicons, and `clock` is authored `evenodd`, so the
 * face has a hole in it only if the icon is told so.
 */
const STROKED = [
  { label: 'Done', path: 'M4 12.5 L9.5 18 L20 6' },
  { label: 'Add', path: 'M12 5 L12 19 M5 12 L19 12' },
  { label: 'Search', path: 'M11 4a7 7 0 1 0 0 14a7 7 0 1 0 0-14 M16 16 L21 21' },
  { label: 'Alerts', path: 'M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6 M10 20a2 2 0 0 0 4 0' }
] as const;

const PLAY =
  'M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 ' +
  '19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z';

const CLOCK =
  'M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 ' +
  '2.25zM12.75 6a.75.75 0 00-1.5 0v6c0 .414.336.75.75.75h4.5a.75.75 0 000-1.5h-3.75V6z';

Props

PropTypeDefaultWhat it does
pathstringrequiredSVG path data, in the coordinates of viewBox.
viewBoxnumber24The side of the square the path was authored in. 24 is the usual icon grid.
sizenumber16The side of the box the icon occupies, in logical pixels.
colorUiColorValuecontrolForegroundA palette name or a colour outright.
style'fill' | 'stroke''fill'A solid glyph, or a line one.
strokeWidthnumber2Line width for a stroked icon, in viewBox units rather than pixels.
fillRule'nonzero' | 'evenodd''nonzero'Which points a filled path encloses.
labelstringnoneWhat a screen reader reads. Omitting it makes the icon decorative.
refUiNodeRefnoneReceives the node the glyph is drawn on.

size sets the box as well as the raster, and flexShrink is fixed at zero, so an icon in a tight row keeps its size and something else gives way. The layout props on the library page apply, and rootModifiers reaches the node the glyph is drawn on.

fillRule matters for any glyph with a hole in it: a clock face, a circle with a slash, an arrow inside a cloud. A subpath wound the same way as the shape containing it does not punch a hole under nonzero and does under evenodd. Icon sets say which they authored for in the SVG's fill-rule, and a path authored evenodd given to a nonzero icon renders as a filled blob.

One path, one colour. A two-colour mark is two Icons stacked, not one with a cleverer path.

Colour is baked in, and that is why it is a prop

Everything else in the library names a palette entry and lets it resolve at paint, against whatever theme the node inherits. An icon cannot: a raster's colour is its pixels. So the modifier behind an Icon reads the theme the node inherits, resolves the palette name against it, rasterises, and rasterises again when a provider above swaps that theme. An icon inside a card that turns dark turns with it, one frame later.

This is why Icon takes a color at all, where a checkbox takes none. The name still goes through the palette, so color="controlAccent" is as themed here as anywhere else; what differs is when it resolves.

What it costs, today: one canvas and one raster per distinct combination of path, size, colour and style. Ten icons in one colour are ten small textures, the same icon in a hover colour is an eleventh, and a theme change redraws the icons on screen and nothing else. Nothing re-rasterises while the specification is unchanged, and released rasters are cached rather than dropped. An atlas would put all of them in one texture; it is not what is in the package, and it would not change any prop above, because what an atlas changes is where the pixels live rather than what an icon is.

Keyboard

None. An icon is not a tab stop, and it is not hit-testable either: the pointer goes straight through it to whatever it sits on. That is what makes an icon inside a button harmless, since the button keeps the hover and the press.

Semantics

WhatValue
Roleimage, when label was given, on the box the glyph is drawn on
Namelabel
StatesNone. A glyph has nothing to be
ValueNone

Most icons should have no label. An icon beside the word it illustrates is decoration, and a screen reader that reads both says everything twice; an icon that is the whole of a button belongs to the button, which is where the name goes. Reach for label when the icon is the only thing carrying the meaning and nothing around it can hold the name.

Given no label the icon has no role and no name, so it is not in the semantics tree at all. As with a picture, that is a decision rather than an oversight: there is no way to get a nameless image record.

Next

Icons is the longer version of the rasterising above, Image is the one for a decoded file, and Toolbar is the row these usually end up in.