Skip to content

Icons

One family, one stroke weight, six sizes. An icon is a mnemonic for something the user already knows — it is not a substitute for telling them.

Also called Icon, Glyph, Pictogram — in this system all of them are Icons.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Core set

Plus

Create something new

X

Close or remove

Search

Find

Settings

Configure

Ellipsis

More actions

Check

Done or selected

ChevronRight

Navigate forward, expand

Trash2

Delete permanently

Pencil

Edit in place

Copy

Duplicate to clipboard

Download

Save to device

Upload

Send from device

Filter

Narrow a result set

RefreshCw

Reload data

Bell

Notifications

User

Account or person

Share2

Send elsewhere

Archive

Remove without deleting

ExternalLink

Opens outside the app

Info

Supplementary detail

Sizes

Six steps. Icons scale sub-linearly with their container — a 1.5× taller button gets a 1.2× larger icon, or the glyph starts to dominate the label.

13pxInside xs controls and badges
14pxInside sm controls, table cells
16pxThe default. Buttons, nav, inputs
18pxlg controls, section headers
22pxFAB, empty-state accents
32pxEmpty states, feature cards

Labelled vs unlabelled

The left row is unambiguous to a first-time user. The right row is a quiz. Both are the same four actions.

LabelledReadable on first encounter
UnlabelledRequires prior learning

Optical alignment

Icons and text share a baseline, not a box. Centring on the line box drops the glyph roughly 1px too low at every size.

Aligned on cap height
Deployment succeeded
Aligned on line box
Deployment succeeded

Interaction states

Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.

Default
Hover
Active
Disabled
Success
Danger
Decorativearia-hidden
Loading

Anatomy

Every part, every measurement, and the reason it is that number.

24×24 viewBox·1.75 stroke·2px padding

Every glyph is drawn on a 24×24 grid with 2px of clearance, so icons of different shapes occupy the same optical area.

  1. Canvas24 × 24

    Fixed viewBox regardless of rendered size. Scaling the viewBox instead of the render size is what makes stroke weights drift between icons.

  2. Live area20 × 20

    2px of padding on every side so a circle and a square read as the same visual weight. Without it, full-bleed glyphs look larger than inset ones.

  3. Stroke weight1.75px at 24px

    Scales with the icon, so a 16px icon renders at ~1.17px. Fixing the stroke in absolute pixels makes small icons look heavy and large ones look spindly.

  4. Corner radius2px, round caps

    Matches the softness of our UI radius scale. Sharp caps in a rounded interface look borrowed from another product.

  5. Optical centre≈1px right of true centre

    Asymmetric glyphs — play triangles, send arrows — need nudging. Mathematical centring makes them look like they are drifting left.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed for
--ds-fg-muted—Decorative and wayfinding icons
--ds-fg-secondary—Interactive icons at rest
--ds-fg—Interactive icons on hover
--ds-accent-text—Active or selected state

Spacing

TokenValueUsed for
icon-xsxs buttons, badges, inline markers
icon-smsm buttons, table cells, chips
icon-mdDefault: buttons, nav, inputs
icon-lglg buttons, section headers
icon-xlFAB, prominent affordances
icon-2xlEmpty states, feature cards
icon gapSpace between an icon and its label

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizeIconLabel gapWhen to use
13px13px6pxInside 28px controls and badges. The floor — below this, strokes disappear.
14px14px6pxInside 32px controls, table cells, chips.
16px16px8pxThe default. 36px buttons, navigation rows, input adornments.
18px18px8px44px buttons, section headers, list leading icons.
22px22px12pxFloating action buttons, prominent affordances.
32px+32–48px16pxEmpty states and feature cards. Never inside a control.

Do

Pair an icon with a label unless the glyph is universalClose, search, add, more and back are learned. Everything else is a guess, and a wrong guess on a destructive action is expensive.
decorative interactive
Mute decorative icons and brighten interactive onesColour separates "this is a signpost" from "this is a button". An icon at full foreground contrast reads as pressable whether it is or not.
Edit — everywhere Delete — everywhere Archive — everywhere
Keep one icon meaning one thingIf the pencil means "edit" on one screen and "annotate" on another, the user has to check every time. A one-to-one mapping is what makes the vocabulary work.
import { Save } from 'lucide-react'import * as Icons from 'lucide-react'
Import icons by nameA namespace import of an icon library defeats tree-shaking and adds several hundred kilobytes of unused SVG to the entry bundle. This is the single most common bundle mistake in React apps.

Don't

Do not mix icon familiesDifferent stroke weights and corner treatments sitting side by side read as a bug. Users cannot name it, but they perceive the product as less carefully made.
Do not rely on a tooltip to explain an iconTooltips need hover, and hover does not exist on touch. On a phone an unlabelled ambiguous icon has no way at all to explain itself.
stroke lost at scale
Do not scale a small icon upA 16px glyph rendered at 40px has a stroke that is proportionally too thin and detail that was optimised away. Use the size the family provides.
Menu item 1 Menu item 2 Menu item 3 Menu item 4 Menu item 5
Do not put an icon on every menu itemWhen everything has an icon, the icons stop distinguishing anything and the column of glyphs becomes noise. Icon the top three actions, or none.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.1.1Non-text ContentA1.4.11Non-text ContrastAA2.5.3Label in NameA

Contrast

  • A meaningful icon must reach 3:1 against its background — it counts as a non-text graphic under WCAG 1.4.11.
  • Purely decorative icons are exempt, but if you cannot say confidently that removing it loses no information, it is not decorative.
  • Thin strokes at small sizes effectively reduce contrast. At 13px, use --ds-fg-secondary rather than --ds-fg-muted.

Keyboard

TabReaches icon buttons in DOM order. A decorative icon is never focusable.
Enter / SpaceActivates an icon button exactly like any other button.

Screen readers

  • The visible label and the accessible name must match. Voice-control users say what they see, so an aria-label of "Remove item" on a button labelled "Delete" is unusable for them.
  • Never use an emoji as a UI icon. Screen readers read the full Unicode name, which is verbose and often absurd in context.
  • Icon fonts are announced as random characters when the font fails to load. Use SVG.

Focus & touch

  • Icon buttons take the standard focus ring. Because they are square and small, the 2px offset matters more than usual — a flush ring on a 32px square is hard to see.
  • A 16px icon inside a 36px button still needs a 44px pointer target. Our IconButton pads the hit area on coarse pointers without changing the visual size.
AttributeApplied toNotes
aria-hidden="true"Decorative iconsAny icon beside a text label. Without it, screen readers may announce a filename or an unhelpful title.
aria-labelIcon-only buttonsRequired. The button has no other accessible name and announces as "button".
role="img" + aria-labelA standalone meaningful iconFor a status glyph that is not inside a control but does carry information.
focusable="false"<svg>Internet Explorer legacy, still worth setting — some tooling makes inline SVG focusable by default.

Code

Example usage

tsx
1// Named imports only — a namespace import kills tree-shaking2import { Save, Trash2, Search } from 'lucide-react'34// Decorative: hidden from assistive tech5<span className="inline-flex items-center gap-2">6  <Save size={16} aria-hidden />7  Save changes8</span>910// Icon-only: aria-label is mandatory11<IconButton label="Delete project" icon={<Trash2 />} />1213// Standalone meaningful icon14<Check size={16} role="img" aria-label="Verified" className="text-success-text" />1516// Sizing scales sub-linearly with the control17const ICON_FOR = { xs: 13, sm: 14, md: 16, lg: 18 } as const18<Icon size={ICON_FOR[size]} />1920// A custom glyph must match the family: 24 viewBox, 1.75 stroke, round caps21export function Custom({ size = 16, ...props }) {22  return (23    <svg24      width={size} height={size} viewBox="0 0 24 24"25      fill="none" stroke="currentColor" strokeWidth={1.75}26      strokeLinecap="round" strokeLinejoin="round"27      aria-hidden focusable="false" {...props}28    >29      <path d="M4 12h16M12 4v16" />30    </svg>31  )32}

CSS

css
/* Icons inherit colour, so one rule themes every glyph */
.icon { stroke: currentColor; fill: none; }

/* Optical alignment: match the cap height, not the line box */
.label-with-icon {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  line-height: 1;          /* stops the line box dragging the icon down */
}

/* Asymmetric glyphs need a nudge inside a circular button */
.btn--play .icon { transform: translateX(1px); }

/* Never let an icon shrink in a flex row */
.icon { flex-shrink: 0; }

/* Spin at a constant rate — a spinner is mechanical, not organic */
@keyframes spin { to { transform: rotate(360deg) } }
.icon--loading { animation: spin 720ms linear infinite; }

@media (prefers-reduced-motion: reduce) {
  .icon--loading { animation-duration: 2s; }
}

Notes

Professional tips

  • Keep a single file that maps every icon in the product to its one meaning. It is the cheapest way to stop the pencil from becoming three different actions.
  • When you need a glyph the family lacks, draw it on the same 24px grid at the same stroke rather than importing a second library for one icon.
  • Icons in a vertical list should share a fixed-width column, so the labels align even when the glyphs have different widths.
  • For directional icons, mirror them in RTL locales — but never mirror a clock, a play button, or anything containing text.

Performance

  • Inline SVG components are tree-shakeable and themeable with currentColor. An SVG sprite is smaller for very large sets but loses per-icon code splitting.
  • Icon fonts should not be used at all: they block rendering, break in high-contrast mode, and announce as garbage characters if the font fails.
  • A component library that imports every icon at the top level ships all of them. Verify with a bundle analyser — this mistake is invisible in development.
  • For a list of 500 rows each with three icons, render the SVG once with <use> references rather than 1,500 inline SVG nodes.

Common mistakes

  • Forgetting aria-hidden on a decorative icon, so a screen reader announces it before every label.
  • Using the same glyph for "remove from list" and "delete permanently". One is reversible; the user cannot tell which.
  • Setting stroke-width in absolute pixels, so 13px icons look heavy and 32px icons look weak.
  • Letting an icon shrink in a flex container because flex-shrink was not set to 0 — the glyph squashes and the whole row looks broken.

Real-world recommendations

  • Test unfamiliar icons by showing five people the glyph alone and asking what it does. Under four correct answers means it needs a label.
  • Icon-only toolbars are appropriate for tools used daily for hours. They are wrong for anything used weekly, and they are always wrong for destructive actions.
  • When adding a new icon, check whether an existing one already carries that meaning. Most icon sprawl comes from not looking first.
  • In localised products, remember that icon conventions are not universal. A shopping trolley, an envelope and a thumbs-up all vary in meaning by region.