---
title: "Icons"
description: "One icon set per design system, one weight, and one rule — outlined when unselected, filled when selected."
url: "https://tanrei.chnotchy.com/foundations/icons/"
last_updated: 2026-10-23T21:17:12.000Z
---

# Icons

> One icon set per design system, one weight, and one rule — outlined when unselected, filled when selected.

Tanrei’s default icons are [Material Symbols](https://fonts.google.com/icons), **Rounded**, at **weight 300**. Like the text faces, the font is referenced by name and not bundled: a product loads it.

How an icon is drawn is a small group of tokens. The icon box comes in three sizes, `sys.icon.small`, `medium` and `large` (16, 20 and 24px by default), and the font’s optical size follows the box.

- **`ref.typeface.icon`**: The icon font. Material Symbols Rounded: a variable font whose weight and fill are set from sys.icon.
- **`sys.icon.family`**: The icon font by selection. A variable icon font (Material Symbols) is one family and tells selected from unselected with its FILL axis; a static icon font tells them apart with two families (Material Icons Outlined / Material Icons). Both default to ref.typeface.icon.
- **`sys.icon.weight`**: The stroke weight of every icon (the \`wght\` axis of the icon font).
- **`sys.icon.fill`**: The fill of an icon by selection (the \`FILL\` axis of the icon font): 0 is outlined, 1 is filled.

Generated from `tokens/ref/typeface.tokens.json`, `tokens/sys/measure.tokens.json`

## The fill rule

**Unselected is outlined. Selected is filled. Always.** A navigation item, a tab, a toggle, a chip, a favourite: the icon stays the same glyph and only its fill changes with the state. Filled and outlined icons are never mixed for decoration, and never chosen by hand — on the web the fill follows the state of the control the icon sits in.

```js
import '@tanrei/web/icons.css';
```

```html
<button aria-pressed="true"><span class="tanrei-icon" aria-hidden="true">favorite</span> Favourite</button>
```

`.tanrei-icon` reads `aria-pressed`, `aria-selected`, `aria-checked` and `aria-current` on any ancestor (on a Tanrei control, which moves them to `data-aria-*`, it reads `data-aria-pressed`, `data-aria-selected`, `data-aria-checked` and `data-aria-current`), or `data-selected` on the icon or an ancestor (`data-selected="false"` opts one icon out). Give an icon `aria-hidden="true"` when text next to it says the same thing, and the control an accessible name when it does not.

The name can also be given in `data-icon`, with the element left empty:

```html
<span class="tanrei-icon" data-icon="folder"></span>
```

The glyph is the same, but its name is no longer text on the page: it is not in `textContent` or `innerText`, not in what people copy, and not in the accessible name of the control around it. Use it wherever the name would otherwise be read as text, such as a row in a menu or a list, a table cell, or text a test compares. Size, selection and right-to-left mirroring work exactly as they do for the text form. Keep `aria-hidden="true"` where the text beside the icon says the same thing: Firefox before 128 still reads the name into the accessible name.

On the native platforms the same values arrive with the design system (`system.icon`), together with a small helper that turns _selected_ and a size into what the font needs. Tanrei does not ship an icon component yet.

## An icon set per design system

The icon set belongs to the design system, so a [layer](https://tanrei.chnotchy.com/foundations/design-systems/) can change it. What the shipped systems resolve to:

| Design system | `family.unselected` | `family.selected` | `weight` | `fill.unselected` | `fill.selected` |
| --- | --- | --- | --- | --- | --- |
| `tanrei` | Material Symbols Rounded | Material Symbols Rounded | 300 | 0 | 1 |
| `cupertino` | Material Symbols Rounded | Material Symbols Rounded | 300 | 0 | 1 |
| `m3-expressive-like` | Material Symbols Outlined | Material Symbols Outlined | 400 | 0 | 1 |
| `m3-like` | Material Symbols Outlined | Material Symbols Outlined | 400 | 0 | 1 |

Generated from `tokens/systems/*.system.json`, `design/tokens/<system>/tanrei.light.tokens.json`

An icon font tells a selected icon from an unselected one in one of two ways, and the tokens cover both. A selected icon always takes `family.selected` **and** `fill.selected`; which of the two actually changes what is drawn depends on the set.

### One variable family

A variable icon font, such as Material Symbols, is a single family with a `FILL` axis. Both family tokens point at `ref.typeface.icon` by default, so re-pointing that one token changes the set, and `fill.unselected` / `fill.selected` tell the states apart. This is all of what `m3-like` writes:

```json
"tokens": {
  "tanrei.ref.typeface.icon": { "$value": ["Material Symbols Outlined"], "reason": "Material Design 3 draws Material Symbols in the Outlined style." },
  "tanrei.sys.icon.weight":   { "$value": 400, "reason": "Material Symbols default weight in Material Design 3." }
}
```

### Two static families

A static icon font has no axes. It ships the outlined and the filled drawings as two separate families, so the layer names one for each state:

```json
"tokens": {
  "tanrei.sys.icon.family.unselected": { "$value": ["Material Icons Outlined"], "reason": "Acme uses the static Material Icons set: outlined when unselected." },
  "tanrei.sys.icon.family.selected":   { "$value": ["Material Icons"], "reason": "Acme uses the static Material Icons set: filled when selected." },
  "tanrei.sys.icon.weight":            { "$value": 400, "reason": "A static icon font has one weight; 400 is what Material Icons is drawn at." }
}
```

The fill and weight tokens stay as they are. A static font ignores the axes, so they do no harm, and a component does not need to know which kind of set is active. This layer is the repository’s own test fixture (`packages/tokens-build/test/fixtures/acme.system.json`).

### Icon names are the product’s concern

Tanrei decides how an icon is drawn, not which icons exist. Sets name their glyphs differently, and a set may lack a glyph another one has. A product that changes its icon set checks the names it uses. A symbol set that is not a font, such as SF Symbols, cannot be named by a family token; that is filed in the mimicry-gaps registry as `platform-symbol-sets`.

### React Native

React Native’s `<Text>` cannot set font-variation axes, so with a variable icon font `FILL` never reaches the glyph and an icon looks the same in both states. To keep the rule there, use two static families as above, or draw the icons as SVG.

The icon helpers of the four native packages are being moved to the family-per-state model now; until a package’s README describes it, expect that package to read a single family.
