Foundations
Icons
Inline SVG on a grid of 16, a currentColor stroke of thickness 1.5. An icon
inherits the colour of the text, which is why the library has not one tone
variant for icons.
Markup
<button class="inst-btn" type="button">
<svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg>
Refresh
</button>
<button class="inst-btn inst-btn--icon" type="button" aria-label="Settings">
<svg class="inst-icon" aria-hidden="true"><use href="#i-settings"/></svg>
</button>
Contract
Five requirements. Breaking any of them shows at once — the icon either slips off the baseline or stops changing colour along with the text.
| Requirement | Why |
|---|---|
Inline SVG (or a <use> from the sprite) |
An icon has to change colour along with the text. <img> and background-image cannot do that |
stroke: currentColor, fill: none |
One file works in all five themes and in every tone without a single variant |
| A stroke thickness of 1.5 | The optics shared with the hairline: 1 reads as a scratch, 2 as a bold face beside a 400 |
A grid of 16 (viewBox="0 0 16 16") |
It matches --size-icon. Another grid gives another optical thickness at the same stroke-width |
| The colour does not live in a data URI | The shape is drawn by a mask and the colour comes as a token. Otherwise the theme never catches the icon up |
The class .inst-icon declares that plus flex: none — otherwise the icon is
the first thing to shrink when the label beside it does not fit.
.inst-icon {
flex: none;
inline-size: var(--icon-size, var(--size-icon));
block-size: var(--icon-size, var(--size-icon));
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}
Markup
| Case | Markup |
|---|---|
| An icon beside a label | aria-hidden="true" on the <svg>. The name is given by the label; without the attribute a screen reader reads the element twice or unintelligibly |
| An icon instead of a label | aria-hidden="true" on the <svg> and aria-label on the button. Otherwise a button with no text has no accessible name |
| An icon inside a link | The same: the name is carried by the link |
| An icon as the only carrier of information | There is no such thing. Add a label or an aria-label — colour and shape have no right to be the only carrier |
Accessibility
| A decorative icon | aria-hidden="true" always, without exception: the name is given by a label or an aria-label |
| A button with no text | Has to carry an aria-label. Otherwise it has no accessible name at all |
| Contrast | An icon inherits the colour of the text, so it passes the same thresholds. An icon on --text-faint is decoration and has no right to carry meaning |
| Colour is not the only carrier | An icon is itself a second carrier of tone (a note), but not the first: the tone has to be in the text as well |
| Forced-colours mode | A currentColor stroke survives the reset, because the colour is taken from the text rather than set to a value of its own |
| Focus | An <svg> never takes focus: what is focused is the button or the link around it, and focusable="false" on the sprite removes the old IE artefact |
Anatomy
The sprite is declared once per document, before the first <use>. It is
itself invisible and hidden from a screen reader.
<svg class="inst-sprite" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
<symbol id="i-refresh" viewBox="0 0 16 16">…</symbol>
</svg>
Use:
<svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg>
The <svg> at the place of use has no viewBox of its own: it comes from
the <symbol>. A viewBox on the outer element beats the symbol's and cuts
part of the glyphs off.
Available ids
The set is deliberately small: an icon is started for a task rather than just in case. The list is printed from the sprite itself — typed by hand it has already parted from the library by four symbols once.
i-booki-charti-checki-chevroni-closei-copyi-enteri-gridi-listi-logouti-mici-morei-movei-plusi-refreshi-rotatei-scalei-settingsi-themei-usersPlus 103 symbols of the reference's own pages (i-p-*) — they belong to the site, not to the kit.
Sizes
| Class or variable | Size | Token | Where |
|---|---|---|---|
inst-icon |
18px | --size-icon |
The default: a button, a menu item, a note, a header |
inst-icon--sm |
10px | --size-chevron |
The chevron of a select, the cross of a tag — a utility glyph |
inst-icon--down |
— | rotate 90deg |
The one chevron, turned: a dropdown affordance on a button |
Which size a chevron takes
--size-chevron is 10px against --size-icon's 18, and the two are not
interchangeable — the token answers three different jobs, and only two of
them are its own.
| The job | Size | Why | Where |
|---|---|---|---|
| A separator between words | --size-chevron |
It stands inside a line of text and is measured against the type | Breadcrumbs, the cascader's path |
| Furniture of a frame — a disclosure that opens what it sits on | --size-chevron |
Measured against the label beside it. The band is 0.62…0.86 of the type size: larger and the arrow competes with the words | The select's arrow, the accordion twist, the tree twist, the sort arrow |
| A glyph in a column of trailing glyphs | --icon-size |
Measured against its actual neighbour, which is another glyph and not a label | The cascader's branch arrow, standing at the same edge as a menu's check mark |
The third row is the one that is easy to get wrong. A .inst-cascader-item
wears .inst-menu-item, whose selection mark is --size-icon; ten against
eighteen in one column read as two vocabularies rather than as one list.
A chevron that is its own target is a different question again, and the
answer is not a smaller glyph but a bigger box. .inst-tab-close states it:
a square from the control ladder with the utilitarian glyph inside, because a
10px cross is a 10px target and a hover fill the size of the sign. Where the
target is real, use a real inst-btn--xs —
a pseudo-element drawn to look like a button is a promise the markup does not
keep.
| --icon-size: var(--size-icon-sm) | 14px | --size-icon-sm | A reading in the status bar: the type size there is --text-2xs |
| --icon-size: var(--size-icon-lg) | 24px | --size-icon-lg | An icon in the rail, where there is no label at all |
All four are one measure with different anchors. An icon beside a label is
measured against ITS cap height, so at the eleven-pixel size the icon has one
of its own; an icon without a label is measured against the tap target, because
there is nothing else to compare it with. cmd/proportion checks this, and the
bands are there too.
An icon larger than these four is either an illustration (its place is in the empty state) or a mistake of hierarchy: an icon serves the data and has to be quieter than it.
The sizes deliberately do not react to density — see density: an icon is a carrier of meaning, and at 12px a stroke of 1.5 turns into a blot.
Composition
<div class="inst-menu" role="menu">
<button class="inst-menu-item" type="button" role="menuitem">
<svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg>Restart
</button>
<button class="inst-menu-item" type="button" role="menuitem">
<svg class="inst-icon" aria-hidden="true"><use href="#i-copy"/></svg>Copy the id
</button>
</div>
An icon button is square: the width is taken from the same control height, so the square holds at all three sizes and at all densities.
Rules
What to take and what not to
| Use | Take instead |
|---|---|
| An icon beside a label — a second carrier of meaning | An icon instead of a label in a list of actions — in a menu the label is obligatory, see the menu in a popover |
An icon instead of a label where the action is common knowledge and there is an aria-label |
An icon instead of a status — a status carries a dot and a word, see the badge |
| An icon as a second mark of tone on a note | Colour as the only carrier — the general rule. Tone is described in colour |
A <use href="#i-*"> from the sprite |
A text glyph ⟳ or ▶ — different metrics in different fonts and the risk of an emoji substitution |
.inst-icon--sm in a chevron and in a small control |
A width of your own inline — the size comes from the roles |
--icon-size on a container when a component needs a size of its own |
A descendant selector .my-block > .inst-icon — if your layer comes before kit.components, the rule will not fire and will not complain |
Customization
- Draw on a 16×16 grid, in strokes only.
- Remove
fill,strokeandstroke-widthfrom the paths themselves — they come from.inst-icon. - Round the caps and joins by the class rather than in the file:
stroke-linecapandstroke-linejoinare already there. - Put it in the sprite as
<symbol id="i-…" viewBox="0 0 16 16">. - Check it on a dark theme: a stroke of 1.5 is optically thinner in a dark theme, and too fine a detail disappears.
API
| Name | What it does |
|---|---|
| class | |
inst-sprite | The sprite in the body of the page. Hides it: without the rule it is a visible block of 150px |
inst-icon | The base one. Always required |
| modifier | |
inst-icon--sm | Reduction to --size-chevron |
inst-icon--down | The same chevron, turned to point down: a dropdown affordance. Clockwise from inline-end, so it lands on down in RTL too |
| Name | Value | Where from |
|---|---|---|
| variable | ||
--icon-sizeThe size of this icon. Set on the CONTAINER and inherited downwards. Through a variable rather than through a descendant selector: the layout lies in an earlier layer, and a selector from there is dead in silence | var(--size-icon-xs) | the application sets it |
| token | ||
--size-iconThe default: an icon beside a label at the base type size | 18px | base +4: scale |
--size-icon-smAn icon at type size --text-2xs: the status bar | 14px | base +4: scale |
--size-icon-lgAn icon WITHOUT a label: the rail of sections. Measured against the tap target | 24px | base +4: scale |
--size-chevronA utility glyph: a chevron, a cross | 10px | base +4: scale |
--stroke | 1px | one across every cell |
Why the kit works this way — the constitution · Open source under MIT, github.com/keshon/instrument