Recommended library

Lucide

Open source, MIT-licensed, comprehensive (1,400+ icons), 1.5–2px stroke geometry that pairs cleanly with Lora's editorial weight. Forks of Feather, with active maintenance. Available as React, Vue, Svelte, web component, or static SVG. lucide.dev

Alternatives considered: Phosphor (warmer, multiple weights — good fit, slightly busier), Tabler (huge, slightly utilitarian), Heroicons (Tailwind-aligned, less editorial), Hugeicons (premium, less open). Lucide wins on geometry + license + ecosystem.

Stroke width

1.5px at 24px viewport

Default. Scales with the icon — at 16px, drop to 1.25px equivalent (Lucide's stroke-width="2" at small sizes); at 32px+, lift to 1.75–2px equivalent. The point is visual consistency, not literal pixels.

Stroke style

Round caps · round joins

Lucide ships this by default. Don't override to square — sharp corners don't pair with Lora's editorial roundness.

Fill

None — outline only

Default to outline icons. Reserve filled variants for active/selected states (e.g., a star toggling to filled when bookmarked). Never mix filled and outline in the same toolbar.

Color

currentColor

Always inherit from color. Never bake a fill into the SVG. This lets the icon tint correctly through state changes.

Four sizes. Use the smallest that reads at distance. Icon size should match the type role it sits with — 16px next to body, 20px next to H4, 24px next to H3, 32px for hero accents.

XS · 16px
--icon-xs
Inline with body, dense UI
SM · 20px
--icon-sm
Buttons, form fields, navigation
MD · 24px (default)
--icon-md
Standalone, near H3, primary controls
LG · 32px
--icon-lg
Hero accents, feature cards
Default --ink-default
Muted --ink-muted
On dark --ink-inverse
On brand surface --c-calm-blue

A baseline 24-icon set covering most product UI. All from Lucide; rendered at MD (24px), --ink-default.

Search
Menu
Close
Check
Arrow→
Chevron↓
Bookmark
Heart
Info
Warning
Error
Clock
Calendar
User
Settings
Message
Phone
Email
Chat
Document
Download
Upload
Trash
Edit
Source

Lucide via package

Engineering: npm i lucide-react (or your framework variant). Design: install the Lucide Figma plugin. Don't paste random SVGs from elsewhere — single source keeps geometry consistent.

Custom icons

Match the Lucide grid

If a needed icon isn't in Lucide, draw it on a 24×24 grid with a 1.5px stroke, round caps/joins, and 2px keylines. Save as inline SVG. Submit to Lucide upstream when possible.

Accessibility

Decorative vs meaningful

Icon paired with a label = decorative. Add aria-hidden="true". Icon alone (icon-only button) = meaningful. Add aria-label="…". Never leave an icon unlabeled.

Touch target

44×44px minimum

The icon can be 20px, but its tappable area is 44×44px. Pad with transparent space — never shrink the touch target.

// React
import { Search, Bookmark, Bell } from 'lucide-react';

// Icon-only button — give it a label
<button aria-label="Search">
  <Search size={20} strokeWidth={1.5} />
</button>

// Icon with text label — decorative, hide from AT
<button>
  <Bookmark size={20} aria-hidden="true" />
  Save for later
</button>

Token integration

/* In tokens.css — already defined */
--icon-xs: 16px;
--icon-sm: 20px;
--icon-md: 24px;
--icon-lg: 32px;
--icon-stroke: 1.5;
Do

Use one icon library per product surface. Lucide everywhere — including admin, marketing, and email where rendering allows.

Don't

Don't mix icon libraries (Material + Lucide + custom). Visual inconsistency reads as carelessness.

Do

Keep stroke geometry consistent — 1.5px at 24px scales proportionally. Use Lucide's defaults.

Don't

Don't override stroke caps to square or join to miter. The brand's roundness extends to icons.

Do

Inherit color via currentColor. Let the parent component drive the tint.

Don't

Don't bake fills into the SVG. It defeats theming and breaks state changes.