Component Library
Reference for the reusable, auto-imported Vue UI components shipped in the packages/ui layer.
Owlat includes a set of reusable Vue components in packages/ui/components/ui/.
When composing these components into a surface, follow the progressive-disclosure standard: lead with a verdict/summary, and keep deeper detail exactly one interaction away.
Overview
All components follow these principles:
- Consistent styling - Uses design system tokens from main.css
- TypeScript - Full type support
- Accessible - ARIA attributes and keyboard navigation
- Auto-imported - Available without explicit imports (Nuxt feature)
Button (UiButton)
Versatile button component with variants and states.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | string | 'primary' | 'primary', 'secondary', 'outline', 'ghost', 'danger', 'danger-ghost', 'danger-outline' |
size | string | 'md' | 'sm', 'md', 'lg' |
loading | boolean | false | Show spinner and disable |
disabled | boolean | false | Disable button |
fullWidth | boolean | false | Full width button |
Slots
default- Button texticonLeft- Icon before texticonRight- Icon after text
Examples
<!-- Primary button -->
<UiButton variant="primary">Save</UiButton>
<!-- With loading state -->
<UiButton variant="primary" :loading="isSubmitting">
Submit
</UiButton>
<!-- With icon -->
<UiButton variant="secondary">
<template #iconLeft>
<Icon name="lucide:plus" class="w-4 h-4" />
</template>
Add Item
</UiButton>
<!-- Danger button -->
<UiButton variant="danger">Delete</UiButton>
Input (UiInput)
Text input with label, error state, and icons.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | string | 'text' | 'text', 'email', 'password', 'number', 'date' |
modelValue | string/number | - | v-model value |
label | string | - | Label text |
placeholder | string | - | Placeholder text |
error | string | - | Error message |
helpText | string | - | Helper text (shown when no error) |
required | boolean | false | Show required asterisk |
disabled | boolean | false | Disable input |
size | string | 'md' | 'sm', 'md' |
Slots
iconLeft- Icon in left side of inputiconRight- Icon in right side of input
Examples
<!-- Basic input -->
<UiInput v-model="email" label="Email" type="email" placeholder="you@example.com" />
<!-- With error -->
<UiInput v-model="email" label="Email" :error="errors.email" required />
<!-- With search icon -->
<UiInput v-model="search" placeholder="Search...">
<template #iconLeft>
<Icon name="lucide:search" class="w-4 h-4" />
</template>
</UiInput>
Select (UiSelect)
Custom dropdown built from a <button> trigger and a floating menu (not a native <select>). Opening shows a transition-animated list with a Lucide chevron on the trigger and a check icon next to the selected option; the menu dismisses on click-outside or Escape. The component is generic over T extends string | number.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | array | [{ value, label }] | |
modelValue | T | null | null | v-model value (string or number) |
label | string | - | Label text |
placeholder | string | 'Select an option' | Text shown when no option is selected |
error | string | - | Error message |
required | boolean | false | Show required asterisk |
disabled | boolean | false | Disable select |
size | string | 'md' | 'sm', 'md' |
Examples
<UiSelect
v-model="country"
label="Country"
placeholder="Select a country"
:options="[
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'de', label: 'Germany' },
]"
/>
Textarea (UiTextarea)
Multi-line text input with character count.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | - | v-model value |
label | string | - | Label text |
placeholder | string | - | Placeholder text |
rows | number | 4 | Number of rows |
maxLength | number | - | Max characters (shows counter) |
resize | string | 'none' | 'none', 'vertical', 'both' |
error | string | - | Error message |
required | boolean | false | Show required asterisk |
Examples
<UiTextarea
v-model="description"
label="Description"
:rows="4"
:max-length="500"
resize="vertical"
/>
Checkbox (UiCheckbox)
Checkbox with label and optional description.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | boolean | false | v-model value |
label | string | - | Label text |
description | string | - | Helper text below label |
disabled | boolean | false | Disable checkbox |
Examples
<UiCheckbox v-model="acceptTerms" label="I accept the terms and conditions" />
<UiCheckbox
v-model="newsletter"
label="Subscribe to newsletter"
description="Receive weekly updates about new features"
/>
Switch (UiSwitch)
Track-and-thumb toggle with role="switch" semantics — the on/off control used across settings, preferences, and config cards. Use UiSwitch for a sliding track toggle; use UiToggle for the icon-style toggle, and keep a bespoke control for tri-state (on/partial/off) feature packs.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | boolean | false | v-model value |
disabled | boolean | false | Disable the switch |
label | string | - | Accessible name — required when no visible label references the switch |
Examples
<UiSwitch v-model="autoReplyEnabled" label="Enable auto-reply" />
<UiSwitch v-model="featureOn" :disabled="!canToggle" label="Knowledge graph" />
Progress Bar (UiProgressBar)
Determinate or indeterminate progress bar with role="progressbar". Used for imports, sending limits, and any unknown-duration work (indeterminate sweep). Honors prefers-reduced-motion.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | 0 | 0–100. Ignored when indeterminate is true. |
variant | string | 'brand' | 'brand', 'success', 'warning', 'error' |
indeterminate | boolean | false | Animated sweep for unknown-duration work |
size | string | 'md' | 'sm', 'md' (track height) |
ariaLabel | string | - | Accessible name announced by screen readers |
Examples
<UiProgressBar :value="importPercent" :aria-label="`Importing: ${importPercent}%`" />
<UiProgressBar indeterminate variant="brand" aria-label="Discovering messages" />
<UiProgressBar :value="95" variant="warning" size="sm" aria-label="Sending limit" />
Card (UiCard)
Container with variants and slots.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
padding | string | 'md' | 'none', 'sm', 'md', 'lg' |
variant | string | 'default' | 'default', 'info', 'warning', 'error' |
hoverable | boolean | false | Add hover border effect |
clickable | boolean | false | Add cursor and emit click |
overflow | string | 'visible' | 'visible', 'hidden' |
Slots
default- Main contentheader- Header with bottom borderfooter- Footer with top border
Examples
<!-- Simple card -->
<UiCard padding="md">
Card content here
</UiCard>
<!-- Card with header and footer -->
<UiCard padding="none">
<template #header>
<div class="px-6 py-4">
<h2>Card Title</h2>
</div>
</template>
<div class="px-6 py-4">
Content here
</div>
<template #footer>
<div class="px-6 py-4 flex justify-end">
<UiButton>Action</UiButton>
</div>
</template>
</UiCard>
<!-- Info callout -->
<UiCard variant="info" padding="md">
<p>This is informational text.</p>
</UiCard>
Modal (UiModal)
Dialog overlay with backdrop.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | v-model:open |
title | string | - | Modal title |
size | string | 'md' | 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', 'full' |
closable | boolean | true | Show close button |
persistent | boolean | false | Disable backdrop click close |
zIndex | number | - | Custom z-index to render above high-z elements like the email builder |
Slots
default- Modal bodyfooter- Modal footer (buttons)
Events
update:open- For v-model
Examples
<template>
<UiButton @click="showModal = true">Open Modal</UiButton>
<UiModal v-model:open="showModal" title="Confirm Action" size="sm">
<p>Are you sure you want to proceed?</p>
<template #footer>
<UiButton variant="ghost" @click="showModal = false"> Cancel </UiButton>
<UiButton variant="primary" @click="handleConfirm"> Confirm </UiButton>
</template>
</UiModal>
</template>
Modal Footer (UiModalFooter)
Footer component for modals with cancel and confirm buttons.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
cancelText | string | 'Cancel' | Cancel button label |
confirmText | string | 'Confirm' | Confirm button label |
confirmVariant | string | 'primary' | Button variant: 'primary', 'secondary', 'ghost', 'danger', 'danger-ghost', etc. |
isLoading | boolean | false | Disables buttons and shows loading state |
isDisabled | boolean | false | Disables confirm button |
Slots
default- Custom footer content (overrides default buttons)
Events
cancel- Cancel button clickedconfirm- Confirm button clicked
Badge (UiBadge)
Status indicator with variants.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | string | 'default' | 'default', 'success', 'warning', 'error', 'neutral' |
size | string | 'sm' | 'sm', 'md' |
dot | boolean | false | Show dot instead of background |
Slots
default- Badge texticon- Icon before text
Examples
<UiBadge variant="success">Active</UiBadge>
<UiBadge variant="warning">Pending</UiBadge>
<UiBadge variant="error">Failed</UiBadge>
<!-- With dot -->
<UiBadge variant="success" dot>Online</UiBadge>
Tabs (UiTabs)
Tab navigation with keyboard support.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | - | v-model (active tab value) |
tabs | array | [{ value, label, count? }] |
Examples
<UiTabs
v-model="activeTab"
:tabs="[
{ value: 'all', label: 'All', count: 25 },
{ value: 'active', label: 'Active', count: 20 },
{ value: 'draft', label: 'Drafts', count: 5 },
]"
/>
Toggle (UiToggle)
Toggle switch using icons.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | boolean | false | v-model value |
label | string | - | Accessible label |
size | string | 'md' | 'sm', 'md' |
disabled | boolean | false | Disable toggle |
Examples
<UiToggle v-model="enabled" label="Enable feature" />
Empty State (UiEmptyState)
Placeholder for empty data.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
icon | string | - | Nuxt/Lucide icon name (e.g. lucide:inbox), rendered via UiIconBox |
title | string | required | Main heading |
description | string | - | Secondary text |
Slots
action- CTA button
Examples
<UiEmptyState
icon="lucide:mail"
title="No emails yet"
description="Create your first email template to get started."
>
<template #action>
<UiButton variant="primary">
<template #iconLeft>
<Icon name="lucide:plus" class="w-4 h-4" />
</template>
Create Template
</UiButton>
</template>
</UiEmptyState>
Error Boundary (UiErrorBoundary)
Catches errors from child components and displays a fallback UI with optional retry.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
fallbackMessage | string | 'Something went wrong. Please try again.' | Custom fallback message |
showRetry | boolean | true | Whether to show a retry button |
Slots
default- Content to render (caught by error boundary)
Examples
<UiErrorBoundary fallback-message="Failed to load contacts.">
<ContactList />
</UiErrorBoundary>
Error Alert (UiErrorAlert)
Alert banner for displaying error, warning, info, or success messages.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
message | string | required | Alert message text |
title | string | - | Custom title (defaults to variant-based title) |
variant | string | 'error' | 'error', 'warning', 'info', 'success' |
Examples
<UiErrorAlert message="Failed to save changes." />
<UiErrorAlert variant="warning" message="Your domain DNS is not yet verified." />
<UiErrorAlert variant="success" message="Campaign sent successfully!" />
Confirmation Dialog (UiConfirmationDialog)
Confirm/cancel modal with customizable title, description, and variant styles.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | - | Controls dialog visibility |
title | string | 'Are you sure?' | Dialog title |
description | string | 'This action cannot be undone.' | Dialog description |
confirmText | string | 'Confirm' | Confirm button text |
cancelText | string | 'Cancel' | Cancel button text |
variant | string | 'default' | 'danger', 'warning', 'default' |
isLoading | boolean | false | Shows loading spinner on confirm button |
persistent | boolean | false | Prevents closing by clicking backdrop |
Slots
default- Custom content above action buttons
Events
update:open- Dialog visibility changedconfirm- Confirm button clickedcancel- Cancel button clicked
Examples
<UiConfirmationDialog
v-model:open="showDeleteDialog"
title="Delete Contact"
description="This will permanently remove the contact and all associated data."
variant="danger"
confirm-text="Delete"
:is-loading="isDeleting"
@confirm="handleDelete"
/>
Dropdown Menu (UiDropdownMenu)
Contextual menu that avoids overflow clipping.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | v-model:open |
position | string | 'right' | 'left', 'right' |
Slots
trigger- Button that opens menudefault- Menu items
Related Components
UiDropdownMenuItem- Menu item with icon, disabled, danger propsUiDropdownDivider- Separator line
Examples
<UiDropdownMenu v-model:open="menuOpen">
<template #trigger>
<button class="btn btn-ghost p-2">
<Icon name="lucide:more-vertical" class="w-4 h-4" />
</button>
</template>
<UiDropdownMenuItem icon="lucide:pencil" @click="handleEdit">
Edit
</UiDropdownMenuItem>
<UiDropdownMenuItem icon="lucide:copy" @click="handleDuplicate">
Duplicate
</UiDropdownMenuItem>
<UiDropdownDivider />
<UiDropdownMenuItem icon="lucide:trash-2" danger @click="handleDelete">
Delete
</UiDropdownMenuItem>
</UiDropdownMenu>
Dropdown Menu Item (UiDropdownMenuItem)
Menu item for use inside UiDropdownMenu.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
icon | string | - | Nuxt/Lucide icon name (e.g. lucide:pencil), rendered via <Icon :name> |
disabled | boolean | false | Disables the menu item |
danger | boolean | false | Applies danger styling |
Slots
default- Menu item label
Events
click- Item clicked
Dropdown Divider (UiDropdownDivider)
Separator line for use inside UiDropdownMenu. No props.
Selectable List Item (UiSelectableListItem)
List item with radio or checkbox selection.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Item label text |
description | string | - | Optional description |
type | string | 'radio' | 'radio' or 'checkbox' |
modelValue | string / boolean / string | - | Current selected value |
value | string | - | Item value |
name | string | - | Input name attribute |
disabled | boolean | false | Disables the item |
Examples
<UiSelectableListItem
v-model="selectedPlan"
value="pro"
label="Pro Plan"
description="Up to 10,000 contacts"
name="plan"
/>
Segmented Control (UiSegmentedControl)
Tab-like segmented selector with animated indicator.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | array | required | [{ value, label, disabled? }] |
modelValue | string | - | Currently selected value |
size | string | 'md' | 'sm', 'md' |
Slots
option-{value}- Custom rendering for a specific option (receivesoptionandactiveprops)
Examples
<UiSegmentedControl
v-model="view"
:options="[
{ value: 'grid', label: 'Grid' },
{ value: 'list', label: 'List' },
]"
/>
Stat Card (UiStatCard)
Statistics display card with value, label, and variant.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string / number | required | Statistic value |
label | string | required | Statistic label |
variant | string | 'default' | 'default', 'success', 'warning', 'error', 'secondary' |
Examples
<UiStatCard value="1,234" label="Total Contacts" />
<UiStatCard value="98.5%" label="Delivery Rate" variant="success" />
<UiStatCard value="12" label="Bounces" variant="error" />
Step Indicator (UiStepIndicator)
Progress indicator showing steps with completion status and connector lines.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
steps | array | required | [{ id, label, number }] |
getStepStatus | function | required | (stepId: string) => 'completed' | 'current' | 'upcoming' |
isConnectorHighlighted | function | required | (index: number) => boolean — determines connector highlighting |
Examples
<UiStepIndicator
:steps="[
{ id: 'domain', label: 'Add Domain', number: 1 },
{ id: 'dns', label: 'Configure DNS', number: 2 },
{ id: 'verify', label: 'Verify', number: 3 },
]"
:get-step-status="getStatus"
:is-connector-highlighted="(i) => i < currentStep"
/>
Toast (useToast)
Global toast notifications via composable.
Usage
const { showToast } = useToast();
// Success (default)
showToast('Saved successfully');
// Error
showToast('Failed to save', 'error');
Toasts auto-dismiss after 3 seconds. Multiple toasts stack vertically.
Icon Box (UiIconBox)
A rounded container holding a single icon, used for empty states, list rows, and section headers. Renders the icon through Nuxt's <Icon>.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
icon | string | required | Nuxt/Lucide icon name (e.g. lucide:users) |
size | string | 'md' | 'xs', 'sm', 'md', 'lg', 'xl' |
variant | string | 'brand' | 'brand', 'success', 'warning', 'error', 'surface', 'inverse' |
rounded | string | 'xl' | 'lg', 'xl', '2xl', 'full' |
Examples
<UiIconBox icon="lucide:users" />
<UiIconBox icon="lucide:check" variant="success" size="sm" rounded="full" />
<UiIconBox icon="lucide:inbox" size="xl" variant="surface" rounded="2xl" />
Setup
UiToast is mounted once in apps/web/app/app.vue for global availability. It is wrapped in <ClientOnly> to avoid an SSR hydration mismatch:
<template>
<div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<!-- Global toast notifications (client-only to avoid SSR hydration mismatch) -->
<ClientOnly>
<UiToast />
</ClientOnly>
</div>
</template>
Theme Toggle (UiThemeToggle)
A single button that cycles the color scheme through System -> Light -> Dark. It uses @nuxtjs/color-mode under the hood and persists the preference automatically. The icon (monitor / sun / moon) reflects the current mode. There are no props; an optional default slot lets you append a text label next to the icon.
Examples
<!-- Icon-only toggle -->
<UiThemeToggle class="p-2 rounded-lg hover:bg-bg-surface" />
<!-- With a label -->
<UiThemeToggle class="flex items-center gap-2">
Theme
</UiThemeToggle>
Chart kit
Hand-rolled SVG charts on design tokens — no chart library. Four components share the same rules:
- One axis only. Never dual-scale; if two series need different scales, use two charts.
- Single series = brand hue (
--color-brand, the defaultcoloron every component). - Multi-series/categorical = the fixed-order palette
--chart-cat-1…--chart-cat-4(CVD- and contrast-validated in both themes). Assign colors in order — never shuffle to "look nice". - Text in text tokens, never series colors. Labels and values are
text-text-*/fill-text-tertiary; color belongs to marks only. - Recessive grids. Grid lines use
--chart-grid, dashed, 0.5 stroke; the baseline is solid. - Tabular numerals everywhere a number can change width.
Trend Chart (UiTrendChart)
Line/area time-series with a recessive dashed grid, y-axis max/mid/min labels, first/last x labels, an endpoint dot, and a crosshair hover with a tooltip.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | { label, value }[] | required | Ordered series (labels are categorical) |
color | string | var(--color-brand) | Line/marker color |
showArea | boolean | true | 10%-opacity fill under the line |
formatValue | (value: number) => string | compact (1.2k) | Axis + tooltip number format |
ariaLabel | string | 'Trend chart' | Accessible name for the SVG |
Examples
<UiTrendChart
:data="days.map((d) => ({ label: d.label, value: d.opens }))"
aria-label="Opens per day over the last 30 days"
/>
<!-- Second categorical series on its own chart, palette order preserved -->
<UiTrendChart :data="clicksSeries" color="var(--chart-cat-2)" :show-area="false" />
Sparkline (UiSparkline)
Inline mini polyline (no axes, no grid) with an endpoint dot, for list rows and stat tiles. ariaLabel is required — the sparkline itself is purely visual.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | number | required | Ordered values |
ariaLabel | string | required | Accessible summary of the trend |
color | string | var(--color-brand) | Line color |
width | number | 88 | Width in px |
height | number | 26 | Height in px |
Examples
<UiSparkline :data="weeklyOpens" aria-label="Opens trending up over the last 8 weeks" />
Stat Tile (UiStatTile)
The chart-kit stat: uppercase muted label, Instrument Serif display numeral (tabular-nums), optional delta line. Use it when the number is the hero of a chart surface; keep UiStatCard for plain colored stat values.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Uppercase tile label |
value | string / number | required | Display value (pre-formatted) |
delta | string | - | Delta text, e.g. '12% vs last week' |
deltaDirection | 'up' / 'down' / 'flat' | 'flat' | up = success, down = error, flat = tertiary |
Examples
<UiStatTile label="Delivered" value="12,480" delta="8% vs last week" delta-direction="up" />
<UiStatTile label="Bounces" :value="bounces" delta="2 more than usual" delta-direction="down" />
Bars (UiBars)
Thin vertical bars anchored to the baseline: rounded data ends, 2px gaps, per-bar tooltip on hover and keyboard focus (opacity-only — zero layout shift), optional sparse x labels. Zero values render as a 2px baseline stub in --chart-grid.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | { label, value }[] | required | Ordered bars |
color | string | var(--color-brand) | Bar fill |
height | number | 128 | Plot height in px (labels add below) |
labelEvery | number | 0 | Show every Nth x label (last always shown); 0 = none |
formatValue | (value: number) => string | toLocaleString() | Tooltip / aria value format |
ariaLabel | string | 'Bar chart' | Accessible name for the chart group |
Examples
<UiBars
:data="sendVolume.map((d) => ({ label: d.label, value: d.count }))"
:label-every="1"
:format-value="(v) => `${v.toLocaleString()} emails`"
aria-label="Emails sent per day over the last 7 days"
/>
Helpers
The pure geometry/format helpers behind the kit live in packages/ui/utils/chart.ts (computeChartPoints, computeYBounds, buildAreaPath, formatChartValue, …) and are unit-tested; reuse them for bespoke SVG charts instead of re-deriving scales.
Micro-interaction kit
Reusable Fluid Functionalism motion drop-ins, defined in packages/ui/assets/css/motion.css and imported by the design-system barrel — available in every app without extra setup. All of them consume the shared motion tokens (--motion-fast/--motion-moderate, --ease-spring) — never hand-written milliseconds — and collapse to opacity-only (or nothing) under prefers-reduced-motion.
Hover reveal (.ui-hover-reveal)
Opacity-only reveal of an action cluster when its row is hovered or focused. The cluster stays in the DOM at full size (opacity, not display) so revealing it never shifts layout, and pointer-events gate with the reveal so invisible actions are never tappable on touch devices. Trigger via Tailwind's group class or an explicit .ui-hover-reveal-host on the row; keyboard focus inside the row (:focus-within) reveals it too.
<!-- DO: cluster inside a hovered/focused host; absolute so it overlays -->
<li class="group relative">
<span>{{ item.title }}</span>
<div class="ui-hover-reveal absolute right-3 top-1/2 -translate-y-1/2 flex gap-0.5">
<button type="button" aria-label="Archive">…</button>
</div>
</li>
<!-- DON'T: v-if / display toggles shift layout and drop keyboard access -->
<li @mouseenter="show = true" @mouseleave="show = false">
<div v-if="show">…</div>
</li>
Spring press (.ui-press)
A scale: 0.98 press on :active, riding --motion-fast + --ease-spring. For bespoke interactive elements (icon buttons, cards acting as buttons) — .btn already carries its own press.
<!-- DO -->
<button type="button" class="ui-press p-2 rounded-lg hover:bg-bg-surface">…</button>
<!-- DON'T: hand-rolled press with hardcoded timing, or doubling up on .btn
(which already presses) -->
<button class="btn btn-primary ui-press">…</button>
<button class="active:scale-95 transition-transform duration-100">…</button>
Staggered entrance (.ui-stagger)
Put on a list container: direct children enter with opacity + a 6px rise at 20ms steps on the moderate tier. Meant for short lists (up to 8 items — queues, action decks, dashboards); later items share the last delay so long lists never feel sluggish. Under reduced motion items simply appear.
<!-- DO: a short, meaningful queue -->
<ul class="ui-stagger">
<li v-for="task in todaysTasks" :key="task.id">…</li>
</ul>
<!-- DON'T: hundreds of virtualized rows — the animation replays on every
scroll-in and delays reading the data -->
<ul class="ui-stagger"><li v-for="msg in allMessages" :key="msg.id">…</li></ul>
Proximity emphasis (.ui-proximity)
For dense icon rows (toolbars, quick-action clusters): the hovered target lifts slightly, its immediate neighbours stay near-full opacity, the rest recede — focus follows the pointer, CSS-only. Keyboard focus (:focus-visible) gets the same emphasis.
<!-- DO: a dense, single-purpose icon row -->
<div class="ui-proximity flex items-center gap-1">
<button type="button" aria-label="Reply">…</button>
<button type="button" aria-label="Archive">…</button>
<button type="button" aria-label="Snooze">…</button>
</div>
<!-- DON'T: on mixed content — receding siblings reads as disabling them -->
<nav class="ui-proximity">…text links and headings…</nav>
Number ticker (UiNumberTicker)
Animated numeral for stat tiles and counters: when value changes, the displayed number rolls to the new value over one --motion-moderate beat. Always tabular-nums so digits never jitter horizontally. Under prefers-reduced-motion it renders plain text that simply updates.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | required | Target value; changes animate |
formatter | (value: number) => string | rounded + grouped | Display format for the in-flight value |
Examples
<!-- DO: hero numeral that updates live -->
<p class="text-2xl font-semibold"><UiNumberTicker :value="stats.sentToday" /></p>
<!-- Custom format -->
<UiNumberTicker :value="deliveryRate" :formatter="(n) => `${(n * 100).toFixed(1)}%`" />
<!-- DON'T: static values that never change (plain text is simpler), or
inside sentences where rolling digits distract from reading -->
<p>You have <UiNumberTicker :value="3" /> drafts.</p>