Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
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). Size tokens xs/sm/md/lg (14/16/20/24px), fixed stroke width, currentColor, aria-hidden unless labeled.
  • 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 runtime icon-role tokens + 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.

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.