Icons
Lucide, outline, 2px stroke, coloured by the text around it. One library — never mix in emoji or another icon set for UI.
The set
lucide-vue-next. Import each icon by name; tree-shaking keeps only what you use. 106 different icons are imported across 72 files — these are the most used, ordered by how many files import them.
ArrowRightForward CTA, “see all”CheckDone, selected, includedMapPinVenue, cityUsersWho’s going, communityXClose, removeCalendarDatePlusAdd to plan, createTicketTickets, passesSparklesNew, recommendedExternalLinkLeaves WeDanceArrowLeftBackLoader2Loading (with animate-spin)UtensilsCrossedFood, cateringChevronDownExpand, dropdownGlobeWebsite, languageMessageCircleChat, ask localsHeartSave, favouriteInstagramInstagram linkGiftFree, perksSearchSearchRotateCcwRetry, resetShare2ShareYoutubeYouTube linkCalendarDaysDate range, scheduleCompassExploreChevronRightRow drill-inSaveSave formPlayPlay videoStarRating, featuredUserPlusInvite, followMusicStyle, DJVideoVideoEyePreview, viewsArrowUpRightOpen detail
Before adding a new icon, check this grid for one that already means the same thing. Browse the full set at lucide.dev/icons.
Sizes
Match the icon to the text it sits with. Always set width and height together (or size-*).
w-3 h-312px · 119×Inside eyebrows, badges and 10–11px meta textw-3.5 h-3.514px · 80×With text-xs / text-sm — chips, small buttons, meta rowsw-4 h-416px · 169×Default. Body text, buttons, inputs, list itemsw-5 h-520px · 43×Icon-only buttons, card headers, navw-6 h-624px · 15×Feature tiles, section iconsw-8 h-832px · 32×Empty states, big feature iconsStroke and colour
Stroke: 2px, always
Lucide’s default. All 476 icon uses in the app keep it — don’t pass stroke-width. Thinner strokes disappear on cream at small sizes.
Colour: inherit the text
Icons draw in currentColor. Colour the parent, not the icon. Use a status colour only when the icon is the status.
Icon + text
inline-flex items-center with a gap — never a margin on the icon, never vertical-align hacks.
<script setup lang="ts">
import { MapPin } from 'lucide-vue-next'
</script>
<span class="inline-flex items-center gap-1.5 text-sm text-muted-foreground">
<MapPin class="w-4 h-4 shrink-0" aria-hidden="true" />
Munich
</span>- Gap: gap-1 with 12px icons, gap-1.5 with 14–16px, gap-2 in buttons.
- Icon first for a label (pin + “Munich”), icon last for direction (“See all” + arrow).
- Add shrink-0 when the text can wrap, so the icon keeps its size.
- For multi-line text, align to the first line:
flex items-start+mt-0.5on the icon.
Icon-only buttons
Allowed for universal actions — close, share, menu, back. Everything else gets a label.
<button type="button" aria-label="Close" class="p-2 rounded-full hover:bg-accent">
<X class="w-5 h-5" aria-hidden="true" />
</button><button aria-label="Close"> <X aria-hidden="true" /> </button>
Do — label the action and hide the icon from screen readers.
<button> <X /> </button>
Don't — ship an icon-only button with no aria-label — screen readers announce “button”.
Accessibility
- Decorative icons next to text get
aria-hidden="true"— the text already says it. - Icon-only buttons and links need an
aria-labelthat names the action (“Close”, “Share festival”), not the icon (“X”). - Give icon-only buttons at least a 40×40px hit area:
p-2around aw-5icon, orp-2.5aroundw-4. - Never rely on an icon alone to carry status — pair Check / X with a word or colour + text.
- Spinners (
Loader2 animate-spin) sit next to a visible “Saving…” label or inside a button witharia-busy="true".