Resolved: light

Iconography

We draw every icon from lucide-react — one library, one visual language. lucide glyphs are plain inline SVG that inherit currentColor and take a strokeWidth, so they re-tint with the theme for free and never need a recolored asset. This page is the governance for that system: the size scale, the stroke weight, the color rule, the decorative-vs-meaningful accessibility rule, and how we name an icon. The catalog at the bottom is the curated set actually used across the app.

library
lucide-react only. import { IconName } from 'lucide-react'. Don't introduce a second icon set, inline a raw <svg>, or paste an emoji where a glyph belongs.
size scale
Three sizes: sm 16px (h-4 w-4), md 20px (h-5 w-5), lg 24px (h-6 w-6). md is the default. Size with the Tailwind h-/w- pair, not the lucide size prop, so it stays on the spacing scale.
stroke weight
Default strokeWidth 2 (lucide's own default) at sm/md. Drop to 1.5–1.75 at 24px and above so large glyphs don't read as heavy. Never go below 1.5.
color
Icons inherit currentColor — tint with a text-* utility (text-muted, text-accent, text-good/warn/bad), never a hardcoded hex. They reskin with the theme automatically.
accessibility
Decorative glyph beside a text label → aria-hidden="true". Icon that is the only label (icon-only button) → give the control an aria-label (or visually-hidden text). See the rule below.
naming
Name by meaning, not by drawing. Map the job (delete, edit, filter) to one canonical glyph and use it everywhere — consistency beats novelty.

Size scale

Three sizes cover the app. Pick by context — dense inline UI gets sm, the everyday default is md, and headers / empty-states / icon chips get lg. Size with the Tailwind height/width pair so icons land on the same spacing grid as everything else.

Dense, inline: table cells, badges, sm buttons, text runs.

sm · 16px · h-4 w-4

The default: buttons, list rows, nav items, field affordances.

md · 20px · h-5 w-5 · default

Prominent: section headers, empty states, IconChip, hero actions.

lg · 24px · h-6 w-6
h-4 w-4 (sm)
16px. Use inline with text and in compact controls. At this size keep strokeWidth at 2.
h-5 w-5 (md)
20px. The default everywhere unless you have a reason otherwise.
h-6 w-6 (lg)
24px. Drop strokeWidth to ~1.75 so the glyph stays light.
why not the size prop
lucide's size prop sets pixels off the spacing scale. Using h-/w- keeps icons on the Tailwind rhythm and lets responsive/variant utilities apply.

Stroke weight

lucide is a stroke (outline) set — the strokeWidth prop sets how heavy each line is. Keep it at 2 for small glyphs; large glyphs at the same weight read chunky, so lighten them. The same Bell below at one fixed size shows the difference.

1.5 — large / display
2 — default
2.5 — too heavy, avoid
Bell at h-7 w-7 · strokeWidth 1.5 / 2 / 2.5
default
strokeWidth 2 — lucide's own default. Leave it alone for sm/md icons; don't set the prop at all.
large glyphs
At 24px+ set strokeWidth to 1.5–1.75 so the icon doesn't dominate next to text.
floor
Never below 1.5 — thinner lines disappear at small sizes and fail contrast.
consistency
Don't vary stroke within a single cluster of icons — a toolbar's glyphs should all match.

Decorative vs meaningful — the a11y rule

Every icon is one of two things to a screen reader, and the choice is not optional. If a visible text label already says what the icon means, the icon is DECORATIVE — hide it. If the icon is the only label, it is MEANINGFUL — give it an accessible name. Get this wrong and you either double-announce (“delete, delete”) or ship an unlabelled button (“button”).

Delete
<button>
  <Trash2 aria-hidden="true" />
  Delete
</button>

The word “Delete” is the name. Mark the glyph aria-hidden="true"so it isn't announced twice.

Decorative — beside a text label
<button aria-label="Delete">
  <Trash2 aria-hidden="true" />
</button>

No visible text, so the control carries the name via aria-label (or a sr-only span). The glyph stays hidden.

Meaningful — icon-only control
decorative → aria-hidden
Icon repeats an adjacent visible label. Set aria-hidden="true" on the <svg> so AT skips it. This is the common case.
meaningful → aria-label
Icon IS the label (icon-only button/link). Put the name on the interactive element via aria-label, or render a visually-hidden sr-only text node. Keep aria-hidden on the glyph itself.
never the only carrier
Don't communicate status with icon (or color) alone — pair it with text or an aria suffix. A red CircleAlert means nothing to a colorblind or screen-reader user without “Error” in words.
tooltips don't count
A hover/focus tooltip is invisible to touch and skimming, so it can't be the accessible name. Label the control directly; the tooltip only adds detail.

Semantic naming

Choose an icon for what it does, not for how it looks, then use that one glyph everywhere the job appears. A reader learns the mapping once; a delete that is sometimes a trash can and sometimes an X has to be relearned every screen. This is the app's canonical action → glyph map.

Meaning in the appGlyphlucide export
Create / addPlus
EditPencil
Delete (destructive)Trash2
SaveSave
Dismiss / closeX
Confirm / doneCheck
Search / findSearch
Filter a listFilter
More actions (overflow)MoreHorizontal
Settings / configSettings
HelpCircleHelp
WarningTriangleAlert
Leaves the app (external)ExternalLink
one job, one glyph
Bind a meaning to a single icon and reuse it. Don't reach for a different drawing of the same idea on a different screen.
follow convention
Use the glyphs people already expect — Trash2 for delete, Pencil for edit, MoreHorizontal for overflow, X for dismiss. Surprise costs comprehension.
alias at the import
When a lucide name reads oddly for the role, alias it where you import: import { MoreHorizontal as OverflowIcon } from "lucide-react". The call site then says what it means.
reserve danger
Keep Trash2 (and the red tone) for genuinely destructive actions, so the signal stays meaningful.

Catalog — 53 commonly-used icons

A curated reference of the glyphs that actually recur across the app, grouped by job. Each cell shows the icon and its exact lucide-react export name — copy the name straight into your import. This is not the whole library (lucide has 1,500+); add to this set deliberately rather than pulling a one-off glyph.

Navigation & layout

Moving around the app and steering the eye.

Menu
Search
ChevronRight
ChevronDown
ArrowRight
ArrowLeft
ExternalLink
LayoutGrid
PanelRight
Home

Actions

Verbs — what a button does. Pair with a text label wherever space allows.

Plus
Pencil
Trash2
Save
Copy
Download
Upload
Send
Filter
RefreshCw
MoreHorizontal
Settings
LogOut

Status & feedback

State of a thing. Always back the glyph with a label or aria text — never color/icon alone.

Check
X
Info
TriangleAlert
CircleAlert
CircleCheck
CircleHelp
Loader2
Bell
Lock

People & contact

Members, contact details and where they are.

User
Users
UserPlus
Mail
Phone
MapPin
Calendar
Building2
Globe
Flag

Domain — rescue & civic

The app's subject matter: the dogs, the volunteering, and the political organizing.

PawPrint
Dog
HandHelping
Megaphone
Vote
ClipboardList
Heart
Star
MessageSquare
Tag