Icon
Single-stroke icons from Lucide, wired to the size + color tokens. Reach for the semantic registry — <Icon semantic="status.success" /> — when an icon represents a known design intent. That's how the system stays one-icon-per-concept across every surface.
01Why Lucide
One stroke style, one weight, one set of design rules. Trade-offs versus the alternatives are below — we chose against Phosphor's 6-weight variety (consistency risk) and Heroicons' 300-icon cap (you'd hit gaps fast given the status / data / system breadth of the product). Lucide is what shadcn ships and what Vercel / Linear / Cal.com run in production.
Lucide ✓
1,500+ icons · single-stroke · MIT · shadcn-native · tree-shakeable
Single weight only — but that's the point.
Phosphor
7,000+ icons · 6 weights (thin/light/regular/bold/fill/duotone)
Weight-mixing risk · bigger bundle.
Tabler
4,500+ icons · outline + filled
Smaller community · less shadcn momentum.
Heroicons
Well-curated · Tailwind-aligned · outline + solid
~300 icons — you'd hit gaps quickly.
Iconify
200,000+ aggregator across many sets
Multiple stroke styles = consistency chaos.
02Size scale
Five sizes mapped to Tailwind utilities. xs/sm fit inline with text; md is the default UI step; lg/xl are for emphasis and hero illustration.
xs
12px
size-3
Micro / dot indicators
sm
14px
size-3.5
Inline with text-sm
md
16px
size-4
Default UI scale
lg
20px
size-5
Emphasis / standalone
xl
24px
size-6
Hero / illustrative
03Tone scale
Tones reach into the color tokens. inherit is the default — the icon picks up surrounding text color. The status tones (success/warning/error/info) match the alert family. Semantic registry entries carry a suggestedTone so the icon picks the right color automatically.
inherit
Adopts surrounding text color (default — recommended for inline icons)
main
icon-main — strongest icon color, pairs with text-main
body
icon-body — softer, pairs with text-body
body-light
icon-body-light — tertiary, pairs with text-body-light
disabled
icon-disabled — disabled state, pairs with text-disabled
muted
UI meta companion (sidebar labels, captions)
white
icon-white — for icons on solid colored fills (primary button, solid success badge, destructive button)
brand
Brand blue — for hero / accent icons on neutral bg
success
Positive status (vivid green on neutral bg)
warning
Caution (amber on neutral bg)
error
Failure / destructive (red on neutral bg)
info
Neutral information (blue on neutral bg)
primary-ink
Icon on bg-primary-subtle (dark blue ink)
success-ink
Icon on bg-success-subtle (dark green ink)
warning-ink
Icon on bg-warning-subtle (dark amber ink)
error-ink
Icon on bg-error-subtle (dark red ink)
info-ink
Icon on bg-info-subtle (dark blue ink)
04Semantic registry
The "blessed" set — one lucide icon per design intent, plus a suggested tone and a usage note. Reaching for <Icon semantic="status.success" /> guarantees that "success" reads as Check everywhere. Search by semantic key, label, lucide name, or use case. Click any card to copy the snippet.
234 icons
05Patterns
Three usage shapes covering the bulk of real-world code.
Inline with text
Recruit pipeline synced.
Use sm or md, default tone (inherit). The icon picks up the surrounding text color so it stays in family.
<p className="flex items-center gap-1.5">
<Icon semantic="status.success" size="sm" />
Recruit pipeline synced.
</p>Icon-only button
Wrap with an accessible name. The Icon itself can carry aria-label, but pairing it with the button's aria-label is the resilient pattern.
<button aria-label="Delete recruit">
<Icon semantic="action.delete" size="sm" />
</button>Trailing CTA arrow
A small ArrowRight that translates on hover signals forward motion. The semantic key navigation.forward keeps it consistent across every CTA in the app.
<button>
View the rubric
<Icon semantic="navigation.forward" size="sm"
className="group-hover:translate-x-0.5
transition-transform" />
</button>Icon on solid colored fill
tone="white" maps to --icon-white. Use it for icons inside primary/destructive buttons, solid status badges — anywhere the surface is a solid colored fill. Inheritance from the parent text color also works, but explicit tone is preferred for readability.
<button className="bg-primary
text-white">
<Icon semantic="action.send" size="sm"
tone="white" />
Send offer
</button>
<button className="bg-error
text-white">
<Icon semantic="action.delete" size="sm"
tone="white" />
Delete recruit
</button>Drawn glyphs — the lucide-gap rule
When lucide lacks a mark, ASSEMBLE it from lucide's own paths — ONE svg in lucide grammar (24 viewBox, stroke 2, round caps), exported from its home component so every surface wears the identical mark. ListTypeGlyph is the one today: lucide ships no list-sparkles, so the ListCard sidebar and the ActivityFeed's list tiles share this drawing — plain = standard list, corner sparkle rising to the second line = smart list. Never a second drawing per surface; swap in the real lucide icon if it ever ships.
import { ListTypeGlyph }
from "@/components/upnext";
<ListTypeGlyph smart={false} />
// standard — the plain list
<ListTypeGlyph smart />
// smart — the corner sparkleIcon on subtle colored bg
The "*-ink" tones map to the matching --*-ink token. Use them when an icon sits on bg-success-subtle, bg-warning-subtle, etc. — alert chips, soft badges, status pills.
<span className="bg-success-subtle
text-success-ink">
<Icon semantic="status.success"
size="xs" tone="success-ink" />
Signed
</span>06Full Lucide library
The escape hatch. Every Lucide icon, searchable. Use this when the registry doesn't carry what you need — and consider adding the icon to lib/icons.ts after you reach for it once or twice. Click any tile to copy the snippet.
3,912 of 3,912 icons
Page 1 of 41