Skip to content
Component installs need the registry setup
VegaStack Design

Icons

Three sanctioned sources — lucide (functional), thesvg (brand), lucide-animated (motion).

Last updated

Icons come from exactly three sanctioned sources, each with a wrapper convention in @vegastack/design/icons:

  • Icon — functional UI icons from lucide-react (every icon). Sizes xs/sm/md/lg (14/16/20/24px), fixed stroke width, currentColor, aria-hidden unless labeled. Inside a component that already sizes its own glyphs — every upstream control does, with [&_svg:not([class*='size-'])]:size-4 — pass the lucide component directly and let the control size it.
  • BrandIcon — brand/logo icons from thesvg (every brand). variant = color (brand colors), auto (uses distinct light/dark artwork when available), mono (inherits currentColor), or light / dark / wordmark.
  • AnimatedIcon — motion icons from lucide-animated, mirrored into the VegaStack registry (467 icons). Add any with shadcn add @vegastack/icon-<name>, then wrap with AnimatedIcon for the same size scale + a11y; the generated mirror animates from pointer, touch, or keyboard focus where its upstream interaction supports replay, or via its React 19 startAnimation() / stopAnimation() ref handle. motion@^13.2.0 is pulled in only when you add an icon — never in the base install.
import { Icon } from "@vegastack/design/icons";
import { Check } from "lucide-react";

<Icon as={Check} size="sm" aria-label="Done" />;
// Animated — after `shadcn add @vegastack/icon-activity`
"use client";
import { AnimatedIcon } from "@vegastack/design/icons";
import { ActivityIcon } from "@/components/ui/icons/activity";

<AnimatedIcon as={ActivityIcon} size="md" aria-label="Activity" />;

The factory owns the controller; icons are data. A mirrored icon is a createAnimatedIcon({ … }) call describing its geometry, its Motion variants, and — where upstream choreography is not a plain play/rest pair — its start/stop steps. Everything shared lives once in @vegastack/design/create-animated-icon: the animation controls, the reduced-motion gate, the imperative handle, and the trigger rules (hover plays on a fine pointer, a tap plays on touch, focus plays and blur rests, and all of them stand down once you attach a ref, so your handler becomes the only driver). The host is an inline-flex <span>, so an icon sits on the text baseline instead of breaking the line.

Reduced motion is intrinsic: playback becomes a no-op and the icon returns immediately to its static resting state. The preference is subscribed to, not sampled once — turning it on settles every icon already on screen, not just the ones mounted afterwards. An explicit <MotionConfig reducedMotion="always"> adds reduction on top of it, so an application that already coordinates its motion centrally gets icons for free — and no MotionConfig is required for VegaStack icon safety.

The override is one-way on purpose. Motion's own default is reducedMotion: "never", and MotionConfig merges over its parent, so an explicit reducedMotion="never" and no MotionConfig at all produce identical context values — nothing can tell them apart. Honouring "never" as an opt-out would therefore switch reduced motion off for every consumer who configured nothing, so the OS preference always wins unless you ask for more reduction, never less.

Attaching a ref makes your handler the only driver, which includes the tap driver: on a touch device there is no hover and no focus, so a ref-controlled icon needs a pointerdown handler of its own or it will never play.

Browse and replay every animated icon. The gallery has its own route so its generated icon modules load only when you ask for the full catalog, not on every docs page.

Leading icons are muted

An icon to the left of a row's text is muted by default (text-muted-foreground; in the sidebar, text-sidebar-foreground/70) and goes to full colour whenever its row does: on hover, focus, highlight, selection, active, open and pressed. The components carry this themselves — dropdown, context and menubar items, select and combobox items, command items, sidebar menu buttons, navigation-menu links, toggle and toggle-group, ghost and outline buttons, breadcrumb links and item media — so do not add colour classes to leading icons. Destructive rows keep text-destructive and coloured alerts keep text-current; tabs, badges, chips, input groups, alerts and empty states are unchanged.

Semantic icons keep their colour

An icon whose colour carries meaning (StatusIcon, PriorityIcon, or any icon you tint with a semantic token) keeps that colour in every state: at rest and on hover, focus, highlight, selection, active, open and pressed. StatusIcon and PriorityIcon carry the marker data-icon-tone on their <svg>, and every muted default and state recolour above skips it ([&_svg:not([data-icon-tone])]). Put data-icon-tone on your own semantic-coloured icon to get the same behaviour; leave it off plain leading icons so they follow the row.

Do
Use Icon / BrandIcon / AnimatedIcon. Keep icons currentColor so they theme automatically.
Don't
Import other icon libraries or inline a raw <svg> as an icon.

On this page