# Icons

> A set of 99 glyphs on one grid, rendered inline so an icon takes the colour and size of the text around it.

`icon()` returns an inline `<svg>`. Every glyph is drawn on the same 24×24 grid as unfilled strokes in `currentColor`, so it inherits the colour and the font size of whatever it sits in and needs no styling of its own.

```javascript
stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('check'),
  icon('search'),
  icon('trash'),
  icon('settings'),
)
```

## In a component

An icon is a child like any other. Because it sizes itself in `em`, it matches the label beside it without being told how big that label is:

```javascript
stack({ direction: 'row', gap: 'sm', align: 'center', wrap: true },
  button({ color: 'primary' }, icon('download'), 'Download'),
  button({ variant: 'outline' }, icon('external-link'), 'Open'),
  button({ size: 'sm', variant: 'soft', color: 'danger' }, icon('trash'), 'Delete'),
  iconButton({ label: 'Search', variant: 'soft', icon: icon('search') }),
)
```

## Size

The default is `1em` — the size of the surrounding text. `size` takes a token or any CSS length when you want to break away from it:

```javascript
stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('star', { size: 'sm' }),
  icon('star'),
  icon('star', { size: 'lg' }),
  icon('star', { size: '2rem' }),
  icon('star', { size: '3rem' }),
)
```

## Colour

There is no colour prop. An icon is drawn in `currentColor`, so it takes the colour of its context — which is what makes one set work inside five palettes:

```javascript
stack({ direction: 'row', gap: 'md', align: 'center' },
  text({ style: 'color: var(--su-primary)' }, icon('heart', { size: 'lg' })),
  text({ style: 'color: var(--su-success)' }, icon('check-circle', { size: 'lg' })),
  text({ style: 'color: var(--su-warning)' }, icon('alert-triangle', { size: 'lg' })),
  text({ style: 'color: var(--su-danger)' }, icon('x-circle', { size: 'lg' })),
  text({ tone: 'muted' }, icon('info', { size: 'lg' })),
)
```

## Accessible names

An icon is `aria-hidden` by default, which is right far more often than not: an icon next to the word “Delete” should not be announced a second time. Give it a `label` only when the icon carries the whole meaning, and it becomes `role="img"` with that name.

```javascript
icon('trash')                      // decorative — hidden
button(icon('trash'), 'Delete')    // the word does the talking

icon('trash', { label: 'Delete' }) // announced as an image

// An icon-only button labels the button, not the glyph inside it
iconButton({ label: 'Delete', icon: icon('trash') })
```

## Filled

`filled` paints a glyph instead of outlining it. It is the same path either way — only the `fill` attribute changes — so the two forms share an outer edge exactly and cannot drift apart.

```javascript
stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('bell', { size: 'lg' }),
  icon('bookmark', { size: 'lg' }),
  icon('folder', { size: 'lg' }),
  icon('heart', { size: 'lg' }),
  icon('star', { size: 'lg' }),
)
```

And the same names filled:

```javascript
stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('bell', { filled: true, size: 'lg' }),
  icon('bookmark', { filled: true, size: 'lg' }),
  icon('folder', { filled: true, size: 'lg' }),
  icon('heart', { filled: true, size: 'lg' }),
  icon('star', { filled: true, size: 'lg' }),
)
```

It is a prop rather than a second set of names because the filled state is nearly always a `state` — saved, liked, rated — so it wants a boolean, not a different string:

```javascript
icon('heart', { filled: liked })
icon('bookmark', { filled: saved, label: saved ? 'Saved' : 'Save' })

// rather than
icon(liked ? 'heart-filled' : 'heart')
```

The status glyphs fill differently, because their mark sits `inside` the shape. Painting the circle would swallow the tick, so the mark is knocked back out of it instead:

```javascript
stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('check-circle', { filled: true, size: 'lg' }),
  icon('x-circle', { filled: true, size: 'lg' }),
  icon('info', { filled: true, size: 'lg' }),
  icon('help', { filled: true, size: 'lg' }),
  icon('alert-triangle', { filled: true, size: 'lg' }),
)
```

Those carry a second drawing — the shape solid with the mark cut out of it by `fill-rule: evenodd` — because a knockout cannot be had from the outline path by changing an attribute. The outer shape is drawn at the outline’s outer edge, so the two forms still end on the same silhouette. It is the same prop either way; which mechanism a glyph uses is its own business.

A chevron has no inside to paint at all — it is an open line — so it fills to the triangle its own three points describe, keeping the stroke that rounds the corners:

```javascript
stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('chevron-up', { filled: true, size: 'lg' }),
  icon('chevron-down', { filled: true, size: 'lg' }),
  icon('chevron-left', { filled: true, size: 'lg' }),
  icon('chevron-right', { filled: true, size: 'lg' }),
)
```

`fillableIcons()` lists everything that answers to `filled`. A glyph without a filled form ignores it and stays outlined — filling `eye` would lose the pupil and `tag` its hole, so neither pretends to.

## Spin

`spin` rotates the glyph — meant for `spinner`, though nothing stops you spinning `refresh` while something reloads. It slows to a crawl rather than stopping under `prefers-reduced-motion`, because a spinner that stops looks broken.

```javascript
stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('spinner', { spin: true, size: 'lg' }),
  icon('refresh', { spin: true, size: 'lg' }),
  button({ variant: 'soft' }, icon('spinner', { spin: true }), 'Saving…'),
)
```

## The set

Names describe the drawing rather than the job it does — `x-circle`, not `error` — because the same drawing gets used for unrelated jobs, and a name that describes the picture stays true when it does. The aliases below cover the common intents.

alert-triangle

arrow-down

arrow-left

arrow-right

arrow-up

banknote

bell

bold

bookmark

calendar

camera

check

check-circle

chevron-down

chevron-left

chevron-right

chevron-up

chevrons-left

chevrons-right

clock

close

code

coins

comment-bubble

copy

credit-card

database

download

edit

external-link

eye

eye-off

facebook

feather

file

filter

folder

gear

gift

globe

google

heart

help

home

image

info

instagram

italic

key

layers

link

linkedin

location

lock

log-in

log-out

mail

menu

minus

moon

more-horizontal

more-vertical

package

percent

pin

plus

print

receipt

refresh

search

settings

share

shopping-bag

shopping-cart

smartphone

sparkles

spinner

star

store

sun

tag

terminal

thumbs-down

thumbs-up

tiktok

trash

truck

universal-access

unlock

upload

user

users

video

wallet

whatsapp

x-circle

x-twitter

youtube

zap

## The square set

`iconSet('square')` draws every glyph again in straight lines — horizontal, vertical or at 45° wherever the thing drawn allows — with square ends, sharp corners and a thinner stroke; a circle is squared off, or cut to an octagon where being round is the point. It is the set the `blueprint` and `terminal` presets are drawn for, and it reaches the icons components draw for themselves too — an alert’s glyph, a dialog’s close button. Call it once, from a module your pages import:

`src/lib/icons.js`

```javascript
import { iconSet } from 'sitelo/ui'

iconSet('square')
```

The names, the aliases and the filled forms are the same in both sets, so no call has to change, and the brand marks stay the brands’ own. `iconSet()` hands back the set it replaced, so it can be put back; called with no name, it only says which one is in use.

Its spinner is a braille cell rather than a ring. Under `spin` its dots run round the cell the way a terminal’s spinner does — ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ — instead of the glyph turning; at rest it shows the first frame, and under `prefers-reduced-motion` it slows down rather than stopping.

alert-triangle

arrow-down

arrow-left

arrow-right

arrow-up

banknote

bell

bold

bookmark

calendar

camera

check

check-circle

chevron-down

chevron-left

chevron-right

chevron-up

chevrons-left

chevrons-right

clock

close

code

coins

comment-bubble

copy

credit-card

database

download

edit

external-link

eye

eye-off

facebook

feather

file

filter

folder

gear

gift

globe

google

heart

help

home

image

info

instagram

italic

key

layers

link

linkedin

location

lock

log-in

log-out

mail

menu

minus

moon

more-horizontal

more-vertical

package

percent

pin

plus

print

receipt

refresh

search

settings

share

shopping-bag

shopping-cart

smartphone

sparkles

spinner

star

store

sun

tag

terminal

thumbs-down

thumbs-up

tiktok

trash

truck

universal-access

unlock

upload

user

users

video

wallet

whatsapp

x-circle

x-twitter

youtube

zap

## Brands

Eight brand marks come with the set — `facebook`, `google`, `instagram`, `linkedin`, `tiktok`, `whatsapp`, `x-twitter` and `youtube`. They still take `size` and `label` and still draw in `currentColor`:

```javascript
stack({ direction: 'row', gap: 'md', align: 'center', wrap: true },
  icon('facebook', { size: 'lg' }),
  icon('instagram', { size: 'lg' }),
  icon('x-twitter', { size: 'lg' }),
  icon('youtube', { size: 'lg' }),
  icon('whatsapp', { size: 'lg' }),
  button({ variant: 'soft', color: 'neutral' }, icon('linkedin'), 'Share'),
)
```

They are reproductions of other people’s marks rather than drawings in this library’s style, so they break two of its rules on purpose: they are solid shapes rather than strokes, which is what a logo is, and their proportions are the brand’s rather than this grid’s. `filled` means nothing to them — they already are.

The artwork is from Simple Icons, which releases it under CC0. That covers the drawing, not the trademark: use these to point at the thing they name — a profile link, a share button — and not on a product of your own.

It is `x-twitter`, not `x`, because `x` already aliases `close` and a close button turning into a logo would be a nasty surprise. `twitter` resolves to it too.

## Aliases

Each of these renders a glyph listed above, under the name you are more likely to reach for:

`success` → `check-circle`

`warning` → `alert-triangle`

`danger, error` → `x-circle`

`x, cross` → `close`

`question` → `help`

`loading` → `spinner`

`cog, gears` → `gear`

`delete, trash-can` → `trash`

`pencil` → `edit`

`notification` → `bell`

`dots` → `more-horizontal`

`bolt, lightning` → `zap`

`arrow-back` → `arrow-left`

`arrow-forward` → `arrow-right`

`cart` → `shopping-cart`

`bag` → `shopping-bag`

`card` → `credit-card`

`cash, money` → `banknote`

`delivery, shipping` → `truck`

`shop` → `store`

`discount, sale` → `percent`

`login, sign-in` → `log-in`

`logout, sign-out` → `log-out`

`map-pin, marker` → `location`

`mobile` → `smartphone`

`like` → `thumbs-up`

`dislike` → `thumbs-down`

`comment, message, chat` → `comment-bubble`

`ai, magic` → `sparkles`

`printer` → `print`

`accessibility, a11y` → `universal-access`

`twitter` → `x-twitter`

## Your own icons

`registerIcons()` adds a glyph, or replaces a built-in. The markup is the `<svg>`’s contents — shapes on the same 24×24 grid, left unfilled so `currentColor` reaches them. Call it once from a module your pages import:

`src/lib/icons.js`

```javascript
import { registerIcons } from 'sitelo/ui'

registerIcons({
  logo: '<path d="M4 20 12 4l8 16z"/>',
  // A name that already exists replaces it everywhere, which is how you
  // restyle a built-in without forking the library.
  check: '<path d="m5 13 4 4 10-11"/>',
  // One closed shape, so it can answer to `filled` like the built-ins.
  pin: { markup: '<path d="M12 21s7-6.3 7-11a7 7 0 1 0-14 0c0 4.7 7 11 7 11z"/>', fillable: true },
})
```

```javascript
import { icon } from 'sitelo/ui'

icon('logo')                  // your glyph
icon('check')                 // now yours as well

registerIcons({ check: null }) // and back to the built-in
```

## Why inline, and not a sprite

Icons render into the page rather than being pulled from an `icons.svg` with `<use>`. A sprite saves on the order of a hundred gzipped bytes of HTML per page and costs a round trip to do it — repeated markup is exactly the case gzip is best at, so most of what a sprite exists to dedupe has been deduped already. Inline also means there is no file to emit, no base path to configure, and nothing that can go missing from `dist` — the same trade `styles({ inline: true })` makes.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | — | Which glyph. Can be passed as the first argument instead. |
| `size` | `'sm' \| 'md' \| 'lg' \| string` | `'md'` | A token, or any CSS length. Default is 1em. |
| `label` | `string` | — | Announce it as an image with this name, instead of hiding it. |
| `spin` | `boolean` | `false` | Rotate it continuously. |
| `filled` | `boolean` | `false` | Paint the glyph rather than outline it. Ignored by glyphs that cannot be filled. |

An unknown name renders nothing at all rather than throwing — a cosmetic prop should not be able to fail a build. `hasIcon(name)` tells you whether one exists, and `iconNames()` lists them all, and `fillableIcons()` the ones that take `filled`.
