One library, one stroke, four sizes. Icons should feel like a quieter sibling of the typography — not the star of the room.
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.
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.
Lucide ships this by default. Don't override to square — sharp corners don't pair with Lora's editorial roundness.
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.
currentColorAlways 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.
--icon-xs
--icon-sm
--icon-md
--icon-lg
--ink-default
--ink-muted
--ink-inverse
--c-calm-blue
A baseline 24-icon set covering most product UI. All from Lucide; rendered at MD (24px), --ink-default.
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.
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.
Icon paired with a label = decorative. Add aria-hidden="true". Icon alone (icon-only button) = meaningful. Add aria-label="…". Never leave an icon unlabeled.
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>
/* In tokens.css — already defined */ --icon-xs: 16px; --icon-sm: 20px; --icon-md: 24px; --icon-lg: 32px; --icon-stroke: 1.5;
Use one icon library per product surface. Lucide everywhere — including admin, marketing, and email where rendering allows.
Don't mix icon libraries (Material + Lucide + custom). Visual inconsistency reads as carelessness.
Keep stroke geometry consistent — 1.5px at 24px scales proportionally. Use Lucide's defaults.
Don't override stroke caps to square or join to miter. The brand's roundness extends to icons.
Inherit color via currentColor. Let the parent component drive the tint.
Don't bake fills into the SVG. It defeats theming and breaks state changes.