Core & Feedback
The everyday presentational building blocks — buttons, cards, badges, stat tiles, avatars, alerts, progress, toasts and navigation aids, every one token-driven and reduced-motion safe.
Overview
These are the reusable presentational primitives that the rest of the template composes with. They live
in src/components/ui/ and are re-exported from a single barrel (src/components/ui/index.ts), so you
import everything from one place:
import {Button, Card, CardBody, Badge, StatCard, Alert, useToast} from '@/components/ui'Every component follows the same house rules:
- Colors come only from semantic tokens (
bg-surface,text-foreground,border-border,bg-primary,success/warning/danger/info…) — never hardcoded hex — so all 13 skins and dark mode work automatically. See Design tokens & dark mode. - Motion is token-driven (
src/lib/motion.ts) and reduced-motion safe — animations tone down or disable when the user prefers reduced motion. See Animation & effects. - Named exports,
interface XxxPropsfor props, andcn()(src/lib/cn.ts) for class composition.
For overlays and disclosure primitives (Modal, Popover, Dropdown, Tabs, Accordion) see Overlays; for the collapsible content Panel see Panel; for form controls see Forms.
Architecture & files
| File | Exports |
|---|---|
src/components/ui/Button.tsx | Button, ButtonSizeProvider |
src/components/ui/Card.tsx | Card, CardHeader, CardBody |
src/components/ui/Badge.tsx | Badge |
src/components/ui/StatCard.tsx | StatCard |
src/components/ui/SectionHeading.tsx | SectionHeading |
src/components/ui/Switch.tsx | Switch |
src/components/ui/Tooltip.tsx | Tooltip |
src/components/ui/Alert.tsx | Alert |
src/components/ui/Progress.tsx | Progress, ProgressRing |
src/components/ui/Avatar.tsx | Avatar, AvatarGroup |
src/components/ui/Skeleton.tsx | Skeleton |
src/components/ui/Carousel.tsx | Carousel |
src/components/ui/Lightbox.tsx | Lightbox |
src/components/ui/CodeBlock.tsx | CodeBlock |
src/components/ui/Pagination.tsx | Pagination |
src/components/ui/Breadcrumbs.tsx | Breadcrumbs |
src/components/ui/BackToTop.tsx | BackToTop |
src/components/ui/RouteProgress.tsx | RouteProgress |
src/components/ui/OptionPills.tsx | OptionPills |
src/components/ui/Toast/ToastProvider.tsx | ToastProvider, useToast |
src/components/ui/Toast/Toaster.tsx | Toaster |
src/components/ui/index.ts | Barrel — re-exports all of the above |
The barrel also re-exports the form/input primitives (Input, Select, Checkbox, DatePicker,
Combobox, Slider, Rating, Stepper, …) — those are documented in Forms. The
overlay/disclosure set (Modal, Popover, Dropdown, Tabs, Accordion) is in
Overlays and Panel in Panel. Note: RichTextEditor is deliberately
not in the barrel (it would hoist Tiptap into the main bundle) — import it directly from
@/components/ui/RichTextEditor in lazy pages.
Live demos of each component are in src/pages/ui/ (ButtonsPage, BadgesPage, AlertsPage,
ProgressPage, CarouselPage, LightboxPage, PaginationPage, NotificationsPage,
TypographyPage, plus IconsPage and WidgetsPage); SectionHeading is demoed across the
src/pages/forms/* pages and CodeBlock on the email-template browser.
Usage
Import from the barrel and drop the component in. Most are pure presentational components with no
provider requirement — the one exception is the toast system, which needs <ToastProvider> in the tree
and a single root-mounted <Toaster/> (already wired in src/main.tsx).
import {Card, CardHeader, CardBody, Button, Badge} from '@/components/ui'
function Example() {
return (
<Card>
<CardHeader title="Team plan" subtitle="Renews monthly" action={<Badge tone="success">Active</Badge>} />
<CardBody>
<Button>Manage subscription</Button>
</CardBody>
</Card>
)
}API / Props
Buttons & actions
Button / ButtonSizeProvider
Primary interactive control. Renders a motion.button with a spring hover/tap scale, so it accepts
all HTMLMotionProps<'button'> (onClick, disabled, type, aria-*, and motion props) plus the
props below.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'outline' | 'ghost' | 'soft' | 'gradient' | 'danger' | 'solid' | 'primary' | Visual style. solid/soft are tone-aware (see tone); the rest are fixed-color. |
tone | 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Status color for the solid / soft variants (ignored by structural variants). |
size | 'xs' | 'sm' | 'md' | 'lg' | 'icon-sm' | 'icon' | 'icon-lg' | resolved: size ?? context ?? 'md' | Control height/padding. icon* sizes are square (h-8/h-10/h-11). |
pill | boolean | false | Fully rounded (capsule) shape. |
loading | boolean | false | Shows a size-matched spinner and disables the button; children stay rendered so width holds. |
className | string | — | Extra classes, merged via cn(). |
| …rest | HTMLMotionProps<'button'> | — | Native button + motion props. |
Status colors are centralized in the component — the tone × variant class maps (solidTone /
softTone in Button.tsx) are the single home for status-button colors, so pages never rebuild
them in className recipes. variant="primary" and variant="danger" are shorthands for
variant="solid" with the matching tone. Solid non-primary tones darken on hover via
hover:brightness-90 (there are no --<tone>-hover tokens) so a hover never falls back to a
surface grey; text uses the *-foreground tokens (never a hardcoded text-white), which keeps
contrast correct on skins like Amber.
ButtonSizeProvider sets a default size for every Button inside it without passing size on each:
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'xs' | 'sm' | 'md' | 'lg' | 'icon-sm' | 'icon' | 'icon-lg' | — | Default size applied to descendant buttons. |
children | ReactNode | — | Content. |
Size resolution is explicit prop ?? context ?? 'md'. PageHeader wraps its action slot in
<ButtonSizeProvider size="sm">, so page-header action buttons should omit size (they become
sm automatically); only set size to override.
OptionPills
A labelled group of single-select "pill" buttons — the shared control behind the Layout Customizer and
Layout Settings pickers (toast position, page transition, page loader, splash frequency). Option labels
resolve in the customizer i18n namespace.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Section label above the pills. |
options | readonly {id: T; key: string}[] | — | Options; key resolves via t() in the customizer namespace. |
value | T | — | Currently selected id. |
onChange | (id: T) => void | — | Selection handler. |
className | string | — | Outer wrapper classes. |
groupClassName | string | — | Pill-container layout classes (e.g. grid grid-cols-2). |
pillClassName | string | — | Extra per-pill classes (e.g. padding density). |
Surfaces
Card / CardHeader / CardBody
The standard rounded, bordered surface (rounded-xl border border-border bg-surface shadow-sm). Card
extends HTMLAttributes<HTMLDivElement>; CardBody is a padded div.
Card
| Prop | Type | Default | Description |
|---|---|---|---|
hover | boolean | false | Subtle shadow raise on hover (no layout shift). |
| …rest | HTMLAttributes<HTMLDivElement> | — | Native div attributes. |
CardHeader (extends Omit<HTMLAttributes<HTMLDivElement>, 'title'>)
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Header title (rendered as h3). |
subtitle | ReactNode | — | Secondary line under the title. |
action | ReactNode | — | Right-aligned slot (button, badge, menu). |
children | ReactNode | — | Extra content under title/subtitle. |
CardBody takes plain HTMLAttributes<HTMLDivElement> (padded p-5).
Switch
Accessible toggle. The whole row is a single role="switch" button — never nest a <button> in a
<label htmlFor> (double-fires). The thumb animates via Motion layout (flips justify-content).
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | — | Controlled on/off state. |
onChange | (checked: boolean) => void | — | Toggle handler. |
label | string | — | Row label. |
description | string | — | Secondary line under the label. |
id | string | — | Id for aria/label association. |
icon | ReactNode | — | Optional leading icon (inside the same button). |
Data display
Badge
Small rounded status pill with a subtle hover scale. Extends
ComponentPropsWithoutRef<typeof motion.span> (accepts className, children, motion props).
| Prop | Type | Default | Description |
|---|---|---|---|
tone | 'neutral' | 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' | Color tone. |
variant | 'soft' | 'solid' | 'outline' | 'dot' | 'soft' | Tinted soft, high-contrast solid, border-only outline, or a surface chip with a status dot. |
size | 'sm' | 'md' | 'lg' | 'md' | Pill padding/text size. |
onRemove | () => void | — | Renders a trailing × button (i18n common:remove label) — makes the badge a removable chip. |
With variant="dot" the badge sits on a bordered surface and the tone colors only the leading dot.
StatCard
Dashboard KPI tile with a count-up value (AnimatedNumber), a pointer-tilt (via the shared
TiltCard wrapper from src/components/motion, max={6}), and an optional delta indicator. It
renders as a staggerItem, so it drops straight into a Stagger grid. All motion is
reduced-motion aware.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Metric label. |
value | number | — | Numeric value (animated count-up). |
icon | LucideIcon | — | Icon shown in the tinted corner badge. |
prefix | string | — | Text before the value (e.g. $). |
suffix | string | — | Text after the value (e.g. k). |
decimals | number | — | Decimal places for the animated number. |
delta | number | — | Percentage change; renders an up/down arrow (success/danger). |
hint | string | — | Muted caption next to the delta. |
SectionHeading
Section divider heading — an icon chip + title with a muted hint — used to group panels within a
page's Stagger grid (see the src/pages/forms/* pages). It renders a StaggerItem, so it drops
straight into the grid; token-only, so it re-skins and dark-modes automatically.
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Heading text (rendered as an h2 in the skin display font). |
hint | ReactNode | — | Muted description under the title. |
icon | ReactNode | — | Optional leading icon, shown in a primary-tinted chip. |
className | string | 'lg:col-span-2' | Wrapper class — defaults to spanning both columns of a 2-col grid; override for other grids (e.g. lg:col-span-3). |
Typography (/ui/typography)
The Typography showcase page (src/pages/ui/TypographyPage.tsx) demonstrates the type system — the
heading scale, article/body text, the type-scale reference table, and the fonts each skin uses. It is
a showcase, not a reusable component; the reusable parts are the skin-aware font utilities defined
in src/styles/index.css:
| Utility | Maps to | Use for |
|---|---|---|
.font-display | var(--font-display) | Headings / display text. Each skin can override the display face (e.g. Console → JetBrains Mono, Bento keeps the default). |
.font-data | var(--font-mono) + tabular-nums + tight tracking | Numeric/data readouts where digits should align in columns. |
.font-numeral | JetBrains Mono (fixed, skin-independent) | Oversized numerals (e.g. the error-page status codes). |
The base UI font is Plus Jakarta Sans (--font-sans); ja/zh swap in Noto Sans JP / SC via
html[lang] overrides. All font vars are tokens, so type re-skins with the active design. Never
hardcode a font family in a component — use these utilities or the --font-* tokens. Full write-up:
Typography (type scale, the showcase-page sections, per-skin & CJK overrides). See
also Design tokens & dark mode for the token definitions and
Design skins for the per-skin font overrides.
Avatar / AvatarGroup
Image avatar with an initials fallback (on missing/broken src) and an optional presence dot.
AvatarGroup overlaps children and collapses overflow into a +N chip.
Avatar
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | — | Image URL. Falls back to initials if absent/broken. |
name | string | — | Used for the alt text and initials. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Avatar size. |
status | 'online' | 'away' | 'busy' | 'offline' | — | Presence dot color. |
className | string | — | Extra classes. |
AvatarGroup
| Prop | Type | Default | Description |
|---|---|---|---|
max | number | 4 | Max avatars before the +N chip. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Size for the overflow chip. |
children | ReactNode | — | Avatar elements. |
className | string | — | Extra classes. |
Info
Template avatars are self-hosted in public/avatars/ (reference as /avatars/avatar-3.png).
Skeleton
Loading placeholder with a shimmer sweep (.skeleton in _skeleton.scss, reduced-motion safe).
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'text' | 'circle' | 'rect' | 'rect' | Shape preset. text is a rounded line; circle is round. |
className | string | — | Sizing/extra classes. |
Carousel
Sliding carousel with arrows, dots, optional autoplay, fade mode, controlled index and drag-to-swipe. Slides animate in the swipe direction; reduced motion swaps all movement for a plain cross-fade and disables autoplay/drag.
| Prop | Type | Default | Description |
|---|---|---|---|
slides | ReactNode[] | — | One node per slide. |
mode | 'slide' | 'fade' | 'slide' | Directional push, or cross-fade in place. |
autoPlay | number | — | Auto-advance every N ms; pauses on hover, disabled under reduced motion. |
showArrows | boolean | true | Prev/next arrow buttons. |
showDots | boolean | true | Dot indicators (active dot stretches; aria-current). |
aspect | string | 'aspect-[16/7]' | Tailwind aspect class for the stage. |
index | number | — | Controlled index — pair with onIndexChange (e.g. a thumbnail strip). |
onIndexChange | (index: number) => void | — | Fires on every slide change. |
className | string | — | Extra classes on the frame. |
Lightbox
Thumbnail grid that opens a full-screen portal viewer with prev/next, a position counter, and keyboard nav (←/→, Esc).
| Prop | Type | Default | Description |
|---|---|---|---|
images | LightboxImage[] | — | Array of {src, alt?}. |
variant | 'grid' | 'masonry' | 'hero' | 'grid' | Trigger layout: uniform grid, span-mixed masonry, or a single 16:9 cover with a "+N photos" badge. |
columns | 2 | 3 | 4 | 4 | Desktop grid columns (grid/masonry variants). |
captions | boolean | false | Show each image's alt as a caption bar in the viewer. |
className | string | — | Extra classes on the thumbnail grid. |
LightboxImage: { src: string; alt?: string }.
Info
The Carousel/Lightbox demo pages use the real landscape photography in public/photos/
(/photos/photo-1.jpg … photo-12.jpg; credits in public/photos/CREDITS.md) — prefer those
over avatar squares for gallery demos. LightboxPage also pairs the gallery with the
ImageRevealSlider motion FX (/photos/photo-8-bw.jpg → photo-8.jpg before/after).
CodeBlock
Editor-style read-only code viewer: a chrome bar (filename "tab" + language Badge + wrap/copy
actions with Tooltips), a line-number gutter, and token-based syntax highlighting via
src/lib/highlightHtml.ts — tokens render as plain text nodes in styled spans (no innerHTML).
Syntax colors are semantic tokens only, so every theme/skin restyles the code automatically. Used by
the email-template browser (src/components/email/EmailSourceModal.tsx).
| Prop | Type | Default | Description |
|---|---|---|---|
code | string | — | The source text. |
filename | string | — | Shown as the editor "tab", e.g. welcome.html. |
language | string | — | Badge label; html is currently the only highlighted language (others render plain). |
defaultWrap | boolean | false | Start with soft-wrapped lines instead of horizontal scroll. |
wrapLabel | string | t('common:wrapLines') | Tooltip label for the wrap toggle. |
copyLabel | string | t('common:copyCode') | Tooltip label for the copy button. |
className | string | — | Extra classes; pass h-full to fill a flex parent (the body scrolls internally). |
Feedback
Alert
Inline, non-dismissable-by-default callout in four tones and four visual variants, with optional
icon override, action slot and close button. Renders role="alert".
| Prop | Type | Default | Description |
|---|---|---|---|
tone | 'info' | 'success' | 'warning' | 'danger' | 'info' | Color + default leading icon. |
variant | 'soft' | 'solid' | 'outline' | 'accent' | 'soft' | Tinted soft, tone-filled solid (on-color *-foreground text), border-only outline, or surface card with a left accent bar. |
title | ReactNode | — | Bold title line. |
children | ReactNode | — | Body content. |
icon | ReactNode | tone icon | Replace the tone's default leading icon; pass null to hide it. |
action | ReactNode | — | Action slot rendered under the body — e.g. small Buttons. |
onClose | () => void | — | When set, shows a dismiss button (i18n common:dismiss) that calls this. |
className | string | — | Extra classes. |
Progress / ProgressRing
Progress is an animated horizontal progress bar, clamped to 0–100; ProgressRing is its circular
SVG sibling. Both expose role="progressbar" with aria-valuenow/min/max and share the same
animation modes: default (spring-fill tracking the prop), animateOnView (fill once when
scrolled into view — for below-the-fold showcases), and loop (a continuous 0→100 cycle whose
value label counts along — one motion value drives both the fill and the label). All motion is
reduced-motion gated (loop/stripes/indeterminate go static).
Progress
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Progress 0–100 (clamped). Ignored while indeterminate. |
tone | 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Bar color. |
size | 'sm' | 'md' | 'lg' | 'md' | Track height; lg is tall enough to carry the value label inside the track. |
showValue | boolean | — | Show the numeric % — right of the track, or inside it when size="lg". |
striped | boolean | — | Animated diagonal stripes over the fill (paused under reduced motion). |
indeterminate | boolean | — | Endless sliding segment for unknown durations (static under reduced motion). |
animateOnView | boolean | — | Fill once when the bar scrolls into view instead of on mount. |
loop | boolean | — | Continuously cycle the fill 0→100 (a live "loading" loop); label counts along. |
loopDelay | number | 0 | Start offset (seconds) for loop — phase-shifts sibling bars out of lockstep. |
className | string | — | Extra classes on the wrapper. |
ProgressRing
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Progress 0–100 (clamped). |
tone | 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Arc color. |
size | 'sm' | 'md' | 'lg' | 'md' | Outer diameter: sm 48 · md 64 · lg 88 px. |
showValue | boolean | — | Show the % in the center. |
animateOnView | boolean | — | Animate the arc when it scrolls into view (once) instead of on mount. |
loop | boolean | — | Continuously cycle the arc 0→100; the center label counts along. |
loopDelay | number | 0 | Start offset (seconds) for loop. |
className | string | — | Extra classes. |
Tooltip
Lightweight CSS-only tooltip — shows on hover and keyboard focus (uses :has(:focus-visible) so a
mouse click doesn't pin it open). Wrap a trigger; the label floats to the chosen side.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Tooltip text. |
children | ReactNode | — | The trigger element to wrap. |
side | 'right' | 'left' | 'top' | 'bottom' | 'right' | Which side the label floats to. |
className | string | — | Extra classes on the wrapper. |
Info
Inside a full-bleed overflow-hidden container, prefer side="bottom" — a top-side label can clip.
Toasts
ToastProvider / useToast / Toaster
Ephemeral notifications. ToastProvider holds the live toast list (capped at 5, newest wins), useToast
fires them, and Toaster renders the stack via a portal. Collapsed cards form a 3D stack that spreads on
hover; content is left-aligned with a tone icon.
Architecture note: <Toaster/> is mounted at the root in src/main.tsx (NOT in AppLayout) so
toasts survive route changes — e.g. a "signed out" toast still shows after navigating to /auth/login.
Its position is driven by config.toastPosition from the Layout Customizer / Layout Settings (an
explicit position prop on <Toaster/> overrides). See
Customizer & settings.
useToast() returns { toast, dismiss }. Fire with const {toast} = useToast():
ToastOptions (argument to toast(...))
| Field | Type | Default | Description |
|---|---|---|---|
title | string | — | Required title. |
description | string | — | Optional secondary line. |
tone | 'neutral' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' | Tone (icon + accent). |
variant | 'default' | 'solid' | 'accent' | 'default' | Card style: surface card, tone-filled solid (on-color *-foreground text), or left accent-bar accent. |
duration | number | 4000 | Auto-dismiss after N ms; pass 0 to keep until dismissed. |
action | {label: string; onClick: () => void} | — | Inline action button (e.g. Undo) — clicking it also dismisses the toast. |
icon | ReactNode | tone icon | Replace the tone icon — e.g. an <Avatar/> for message-style toasts. |
progress | boolean | — | Show a countdown bar that drains over duration (hidden when sticky / reduced motion). |
toast(opts) returns the numeric id; dismiss(id) removes it early.
Toaster props
| Prop | Type | Default | Description |
|---|---|---|---|
position | ToastPosition | config.toastPosition | 'center' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-right'. Overrides layout config when set. |
Never clip toast text
The title and description wrap (break-words, no truncate/
line-clamp) so the full message is always readable — don't add truncation.
The /ui/notifications showcase page
src/pages/ui/NotificationsPage.tsx (route /ui/notifications, sidebar Components → UI Elements →
Notifications) is the live demo of the toast system — a gallery of buttons that fire example toasts so
you can preview every tone, variant, and behavior. It's a showcase, not a component; the reusable API
is useToast() documented above. Its sections:
| Section | Fires | Demonstrates |
|---|---|---|
| Tones | toast({tone}) for neutral / info / success / warning / danger | The five tones (icon + accent). |
| With action | toast({action: {label, onClick}}) | An inline action button (e.g. Undo) that also dismisses the toast. |
| Card styles | toast({variant}) for default / solid / accent | The three card treatments. |
Position is not a per-page control here — every toast lands at config.toastPosition (top-center by
default). Change it in the Customizer or Layout Settings, or use
the Toast Position page-layout preview (/page-layouts/toast-position) to try positions live. See
Page-Layout Previews.
Navigation aids
Breadcrumbs
Breadcrumb trail. Pass explicit items, or omit them to auto-derive the trail from the current route
against the sidebar menu (single source of truth). Used by PageHeader.
| Prop | Type | Default | Description |
|---|---|---|---|
items | Crumb[] | auto-derived | Explicit crumbs; omit to derive from the route. |
className | string | — | Extra classes. |
Crumb: { label: string; to?: string }.
Pagination
Page navigator with … gaps around the current page. Returns null when there's a single page.
Accessible markup: a <nav aria-label> wrapper, aria-current="page" on the active page button,
translated common:prevPage/nextPage labels, and the minimal variant renders "Page X of Y"
via <Trans i18nKey="common:pageOf"> so the bold page number survives translation.
| Prop | Type | Default | Description |
|---|---|---|---|
page | number | — | Current page (1-based). |
pageCount | number | — | Total pages. |
onPageChange | (page: number) => void | — | Called with the clamped target page. |
siblings | number | 1 | Sibling pages shown either side of the current one. |
variant | 'default' | 'pill' | 'outline' | 'minimal' | 'default' | Spaced buttons, rounded pills, a joined outline group, or minimal prev/next + "Page X of Y". |
size | 'sm' | 'md' | 'md' | Control dimensions. |
className | string | — | Extra classes. |
BackToTop
Floating "back to top" button that appears once the window scrolls past ~400px and smooth-scrolls to the
top. No props — it reads window scroll via useScroll. Mounted once in AppLayout.
RouteProgress
Thin top loading bar that animates across on each route change (keyed on pathname so it replays). Returns
null under reduced motion. No props — mounted once in AppLayout.
Configuration & customization
- Restyle via tokens, not props. Colors/radii come from CSS custom properties in
src/styles/index.css; changing a token re-skins every component. See Design tokens & dark mode. - Extend classes with
className. Every component merges extra classes viacn()(clsx + tailwind-merge), so later utilities win over defaults. - Motion values live in
src/lib/motion.ts(spring,springSnappy,staggerItem,EASE_OUT); reuse them instead of hardcoding numbers. - Toast position is a layout setting (
config.toastPosition), not a per-call option.
Examples
Buttons with the size context (page-header pattern):
import {ButtonSizeProvider, Button} from '@/components/ui'
<ButtonSizeProvider size="sm">
<Button variant="outline">Export</Button>
<Button>New item</Button> {/* inherits sm */}
</ButtonSizeProvider>Status buttons via the tone-aware variants (never rebuild these colors in className):
<Button variant="solid" tone="success">Approve</Button>
<Button variant="soft" tone="warning">Snooze</Button>
<Button variant="solid" tone="danger" loading={deleting} onClick={remove}>Delete</Button>Looping progress trio (phase-shifted so they don't cycle in lockstep):
import {Progress, ProgressRing} from '@/components/ui'
<Progress value={72} showValue loop loopDelay={0} />
<Progress value={45} tone="success" showValue loop loopDelay={0.25} />
<ProgressRing value={88} tone="info" size="lg" showValue animateOnView />A stat tile with delta and count-up:
import {StatCard} from '@/components/ui'
import {DollarSign} from 'lucide-react'
<StatCard label="Revenue" value={48250} prefix="$" delta={12.4} hint="vs. last month" icon={DollarSign} />Firing a toast from anywhere in the tree:
import {useToast} from '@/components/ui'
function SaveButton() {
const {toast} = useToast()
return (
<button onClick={() => toast({title: 'Saved', description: 'Your changes are live.', tone: 'success'})}>
Save
</button>
)
}Alert with dismiss + a progress bar:
import {Alert, Progress} from '@/components/ui'
<Alert tone="warning" title="Storage almost full" onClose={() => setHidden(true)}>
You've used 92% of your plan.
</Alert>
<Progress value={92} tone="warning" showValue />Avatar stack and a lightbox gallery:
import {Avatar, AvatarGroup, Lightbox} from '@/components/ui'
import {asset} from '@/lib/asset'
<AvatarGroup max={3}>
<Avatar name="Jane Cooper" src={asset('/avatars/avatar-1.png')} status="online" />
<Avatar name="Cody Fisher" src={asset('/avatars/avatar-2.png')} />
<Avatar name="Esther Howard" />
</AvatarGroup>
<Lightbox images={[{src: '/img/a.jpg', alt: 'A'}, {src: '/img/b.jpg'}]} />Best practices
- Import from the barrel
@/components/ui, not the individual files (exception:RichTextEditor, which is kept out of the barrel — import it directly in lazy pages). - Never hardcode a color — add or use a token so light/dark and all skins stay in sync.
- Status-colored buttons go through
variant="solid|soft"+tone— don't rebuild tone colors in per-pageclassNamerecipes (the maps inButton.tsxare the single source). - Never nest interactive elements (a
<button>inside aSwitchrow, or inside anAvatarbutton). - On page-header actions, omit
size(theButtonSizeProvidermakes themsm). - Fire toasts through
useToast(); keep messages short but let them wrap — never truncate them. - Mount
<Toaster/>,<BackToTop/>, and<RouteProgress/>exactly once (already done in the shell). - Wrap user-facing strings in i18n
t()— see Getting started.
Troubleshooting
| Symptom | Likely cause & fix |
|---|---|
useToast must be used within <ToastProvider> | The component isn't under ToastProvider. It wraps the app in src/main.tsx; ensure your tree is inside it. |
| Toasts don't appear | No <Toaster/> mounted, or it's mounted inside a route that unmounts. It belongs at the root in main.tsx. |
| Toast text is cut off | Something added truncate/line-clamp. Toast text must wrap (break-words) — remove the clamp. |
| Tooltip stays open after clicking | Expected only with focus-within; this component uses :has(:focus-visible) to avoid it. If you see it, you're on a custom tooltip. |
| Avatar shows initials, not the image | src is missing or failed to load — the fallback is intentional. Check the path (/avatars/...). |
| Colors look wrong in dark mode / a skin | A hardcoded hex slipped in. Replace with a semantic token. |
| Pagination renders nothing | pageCount <= 1 returns null by design. |
FAQ
How do I change where toasts appear? Set the toast position in the Layout Customizer or Layout
Settings — it's driven by config.toastPosition. Only pass a position prop to <Toaster/> to force it.
Can Button be a link? It renders a motion.button. For navigation, wrap a router Link or use an
onClick with navigate().
How do I show a spinner-style loading state? Use Skeleton placeholders shaped like the content
(preferred over spinners in this template), or Progress for determinate progress.
Does StatCard require a chart library? No — the count-up is AnimatedNumber from
src/components/motion, free-core Motion only.
Notes for designers & content editors
- Tones map to meaning:
success(green),warning(amber),danger(red),info(blue),primary(brand),neutral(muted). Use them consistently across badges, alerts, and toasts. - All copy is translatable. Labels, titles, and toast messages should be i18n keys, not literals (proper nouns and numbers stay literal).
- Keep toast messages complete — they wrap and are never truncated, so write the full sentence.
- Icons come from
lucide-react; pick a matching icon for stat tiles and alerts.
Related
Was this page helpful?
