Icon
A Material Symbols Rounded UI icon, drawn as inline SVG and filled in currentColor, so it takes the ink of the part it sits in.
import { Icon } from '@fairgarden-private/design/components/Icon'
Icons are per-icon SVG paths generated from @material-symbols/svg-400 to svg-700, so there is no icon font to load. Give an icon a label when it stands alone; beside a visible label or inside a named control, omit it and the icon is hidden from assistive technology.
Size tiers
Size tiers
Three tiers, never scaled in between: inline 16 px beside text, tag 20 px in tags and chips, block 36 px in feature blocks. Each tier draws the Material Symbols weight whose stroke matches its line weight, so an icon sits at the same weight as the rules and edges around it.
import { Icon } from '@fairgarden-private/design/components/Icon'
export function IconSizes() {
return (
<>
<Icon name="search" size="inline" label="Search, inline (16 px)" />
<Icon name="search" size="tag" label="Search, tag (20 px)" />
<Icon name="search" size="block" label="Search, block (36 px)" />
</>
)
}
Weights
Rest and emphasis weights
The weight follows the tier. weight="emphasis" draws the next stroke weight at the same size, as the weight-change cue of a pressed or selected state; the glyph never grows.
| Tier | Size | Target stroke | Rest weight | Emphasis weight |
|---|---|---|---|---|
inline | 16 px | --ds-stroke-1-25 | 600 (1.32 px) | 700 (1.57 px) |
tag | 20 px | --ds-stroke-1-5 | 500 (1.42 px) | 700 (1.96 px) |
block | 36 px | --border-size-2 | 400 (2.25 px) | 600 (2.97 px) |
The weights were measured from the packages' stems, not guessed; iconTierWeights in @fairgarden-private/design/icons/paths holds them.
inline 16 px600700tag 20 px500700block 36 px400600import { Icon } from '@fairgarden-private/design/components/Icon'
import { iconTierWeights } from '@fairgarden-private/design/icons/paths'
import styles from './weights.module.css'
const tiers = [
{ size: 'inline', px: 16 },
{ size: 'tag', px: 20 },
{ size: 'block', px: 36 },
] as const
/** Each tier at rest and at emphasis: the stroke steps up one tier, the size never changes. */
export function IconWeights() {
return (
<div className={styles.grid}>
<span className={styles.head}>Tier</span>
<span className={styles.head}>Rest</span>
<span className={styles.head}>Emphasis</span>
{tiers.map(({ size, px }) => (
<div key={size} className={styles.row}>
<code className={styles.name}>
{size} {px} px
</code>
<span className={styles.sample}>
<Icon name="search" size={size} />
<Icon name="close" size={size} />
<code className={styles.weight}>{iconTierWeights[size].rest}</code>
</span>
<span className={styles.sample}>
<Icon name="search" size={size} weight="emphasis" />
<Icon name="close" size={size} weight="emphasis" />
<code className={styles.weight}>{iconTierWeights[size].emphasis}</code>
</span>
</div>
))}
</div>
)
}
Hover and press
Hover and press
An icon inside a control thickens on hover instead of changing color or size. Give the control the iconHost class and the icon weight="interactive": the icon swaps to its emphasis weight while the control is hovered or pressed, and keeps its rest weight while the control is disabled or busy. In a CSS module, compose the class instead:
.base {
composes: iconHost from '@fairgarden-private/design/components/Icon/icon.module.css';
}import { Icon, iconHost } from '@fairgarden-private/design/components/Icon'
import styles from './hover.module.css'
/**
* `weight="interactive"` draws the rest weight and swaps to emphasis while
* the element carrying `iconHost` is hovered or pressed. A disabled host
* keeps the rest weight.
*/
export function IconHover() {
return (
<div className={styles.row}>
<button type="button" className={`${iconHost} ${styles.button}`}>
<Icon name="close" weight="interactive" label="Close" />
</button>
<button type="button" className={`${iconHost} ${styles.button}`}>
<Icon name="menu" size="tag" weight="interactive" label="Menu" />
</button>
<a href="#hover" className={`${iconHost} ${styles.link}`}>
See all
<Icon name="chevron_right" weight="interactive" className={styles.glyph} />
</a>
<button type="button" disabled className={`${iconHost} ${styles.button}`}>
<Icon name="add" weight="interactive" label="Add (unavailable)" />
</button>
</div>
)
}
The inventory
The inventory
The UI inventory, by Material Symbols name. Glyphs that point along the inline axis (arrow_forward, chevron_left, chevron_right) mirror in right-to-left layouts.
searcharrow_forwardarrow_upwardexpand_morecloseremoveaddcheckcirclechevron_rightchevron_leftmenumore_horizplay_arrowpausedownloadzoom_inzoom_outrecenterhelpmail
import { Icon } from '@fairgarden-private/design/components/Icon'
import { iconNames } from '@fairgarden-private/design/icons/paths'
import styles from './inventory.module.css'
export function IconInventory() {
return (
<ul className={styles.grid}>
{iconNames.map((name) => (
<li key={name} className={styles.cell}>
<Icon name={name} size="tag" />
<code className={styles.name}>{name}</code>
</li>
))}
</ul>
)
}
On grounds
On paper, forest and leaf
Icon has no primary or secondary props. Its color is its part's ink role, so it follows the ground and the page mode without a selector of its own.
import { PresetGround } from '@/components/PresetGround'
import { Icon } from '@fairgarden-private/design/components/Icon'
import styles from './grounds.module.css'
const presets = ['paper', 'forest', 'leaf'] as const
/** No color props: the icon is filled in currentColor, so it takes the ink around it. */
export function IconGrounds() {
return (
<div className={styles.row}>
{presets.map((preset) => (
<PresetGround key={preset} preset={preset} className={styles.face}>
<Icon name="check" size="block" label="Checked" />
<span className={styles.name}>{preset}</span>
</PresetGround>
))}
</div>
)
}
API Reference
A Material Symbols Rounded UI icon, drawn as inline SVG in currentColor
(FILL 0) at its tier’s calibrated weight. The one FILL 1 instance,
circle, is a selection mark only and must always travel with a second
cue (a bar, a weight change, an edge change or a word): state is never
shown by fill alone [D166].
| Prop | Type | Description |
|---|---|---|
name | | The §6.10 inventory name (Material Symbols Rounded). |
label | | Accessible name. Omit it for an icon beside a visible label or inside a named control: the icon is then hidden from assistive technology. |
weight | |
|
icon
iconHost
The host class for weight="interactive" icons (§10.1 icon states). Add it
to the element whose hover or press swaps the icon to its emphasis weight
(a button, a cell, a link), or compose it in the host’s module:
composes: iconHost from '../Icon/icon.module.css'. Kept out of the
client module, so a server component reads the class name itself.
type iconHost = stringIconName
type IconName =
| 'menu'
| 'search'
| 'circle'
| 'arrow_forward'
| 'arrow_upward'
| 'expand_more'
| 'close'
| 'remove'
| 'add'
| 'check'
| 'chevron_right'
| 'chevron_left'
| 'more_horiz'
| 'play_arrow'
| 'pause'
| 'download'
| 'zoom_in'
| 'zoom_out'
| 'recenter'
| 'help'
| 'mail'IconProps
Props for Icon: SVG props (without children, color, fill and size), the size tier, the name and the weight state.
type IconProps = Omit<React.SVGProps<SVGSVGElement>, 'children' | 'color' | 'fill' | 'size'> &
VariantProps<__type> & {
name:
| 'menu'
| 'search'
| 'circle'
| 'arrow_forward'
| 'arrow_upward'
| 'expand_more'
| 'close'
| 'remove'
| 'add'
| 'check'
| 'chevron_right'
| 'chevron_left'
| 'more_horiz'
| 'play_arrow'
| 'pause'
| 'download'
| 'zoom_in'
| 'zoom_out'
| 'recenter'
| 'help'
| 'mail';
label?: string;
weight?: IconWeightState;
}IconTier
type IconTier = 'inline' | 'tag' | 'block'IconWeightState
Which weight instance an icon draws (§10.1 icon states) [D166]:
rest: the tier’s calibrated weight;emphasis: the next stroke tier’s weight at the same size, pinned;interactive: rest, swapping to emphasis while aniconHostancestor is hovered or pressed.
type IconWeightState = 'rest' | 'emphasis' | 'interactive'Specification: DESIGN-SYSTEM.md §6.10 (icons) and §1.5.12 (sizing).