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.
The default: buttons, list rows, nav items, field affordances.
Prominent: section headers, empty states, IconChip, hero actions.
- 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 / display2 — default2.5 — too heavy, avoid- 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”).
<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.
<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.
- 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 app | Glyph | lucide export |
|---|---|---|
| Create / add | Plus | |
| Edit | Pencil | |
| Delete (destructive) | Trash2 | |
| Save | Save | |
| Dismiss / close | X | |
| Confirm / done | Check | |
| Search / find | Search | |
| Filter a list | Filter | |
| More actions (overflow) | MoreHorizontal | |
| Settings / config | Settings | |
| Help | CircleHelp | |
| Warning | TriangleAlert | |
| 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.
MenuSearchChevronRightChevronDownArrowRightArrowLeftExternalLinkLayoutGridPanelRightHomeActions
Verbs — what a button does. Pair with a text label wherever space allows.
PlusPencilTrash2SaveCopyDownloadUploadSendFilterRefreshCwMoreHorizontalSettingsLogOutStatus & feedback
State of a thing. Always back the glyph with a label or aria text — never color/icon alone.
CheckXInfoTriangleAlertCircleAlertCircleCheckCircleHelpLoader2BellLockPeople & contact
Members, contact details and where they are.
UserUsersUserPlusMailPhoneMapPinCalendarBuilding2GlobeFlagDomain — rescue & civic
The app's subject matter: the dogs, the volunteering, and the political organizing.
PawPrintDogHandHelpingMegaphoneVoteClipboardListHeartStarMessageSquareTag