Overlays
Dialogs, floating panels, notifications and progress indicators. Everything that covers the page traps focus, locks the page behind it, and hands focus back to whatever opened it — and everything that floats beside an anchor flips to the other side rather than running off the screen.
Alert
An inline message inside the flow of a page. Warnings and errors carry role="alert" so screen readers interrupt; info and success wait their turn. The status colour never touches the body text — it stays readable on every skin.
Four levels
Seat held
Booking confirmed
Departure delayed
Payment failed
<UiAlert variant="warning" title="Departure delayed">Dismissible, with an action
Unsaved changes
<UiAlert v-model:open="open" dismissible>| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | true | Two-way, written as v-model:open. |
| variant | 'info' | 'success' | 'warning' | 'danger' | 'info' | Sets colour, icon and ARIA role. |
| title | string | undefined | Bold first line. |
| icon | string | per variant | Overrides the variant icon. |
| dismissible | boolean | false | Shows the close button. |
Banner
A full-width bar for things that are true of the whole app rather than one panel: which environment you are on, an expiring plan, a maintenance window. It sits below dialogs on purpose — an announcement should never cover a decision.
Environment notice
You are looking at staging data. Bookings made here are not real.
Scheduled maintenance on Sunday, 02:00–04:00 WIB.
<UiBanner variant="warning" sticky>| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | true | Two-way, written as v-model:open. |
| variant | 'info' | 'success' | 'warning' | 'danger' | 'info' | Colour and icon. |
| icon | string | per variant | Overrides the variant icon. |
| dismissible | boolean | true | Shows the close button. |
| sticky | boolean | false | Pins the bar to the top of the viewport. |
Confirm dialog
Confirming does not close the dialog — the caller does, once the work finishes. That is what lets the button show a spinner instead of the page appearing to do nothing.
Destructive confirmation
<UiConfirmDialog v-model:open="open" tone="danger" @confirm="hapus" />| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | false | Two-way, written as v-model:open. |
| title | string | — | Question being asked. |
| message | string | — | Consequence of confirming. |
| confirmLabel | string | 'Confirm' | Label of the confirming button. |
| cancelLabel | string | 'Cancel' | Label of the dismissing button. |
| tone | 'primary' | 'danger' | 'primary' | Visual weight of the confirming button. |
| loading | boolean | false | Keeps the dialog open while the work runs. |
Drawer
A panel anchored to one edge, for filters and detail views that would crowd a dialog. It traps focus and locks the page exactly like the modal does.
Four edges
<UiDrawer v-model:open="open" side="right" title="Filters" />| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | false | Two-way, written as v-model:open. |
| side | 'left' | 'right' | 'top' | 'bottom' | 'right' | Edge the panel is anchored to. |
| title | string | undefined | Heading, also the accessible name. |
| closable | boolean | true | Shows the close button. |
| dismissible | boolean | true | Escape and backdrop clicks close it. |
Dropdown
A menu button. Arrow keys open it and move through the entries, Home and End jump to the ends, disabled entries are skipped rather than landed on, Enter picks, Escape closes. Focus never leaves the button — the highlighted entry is announced through aria-activedescendant, so there is no focus to hand back.
Row actions
Nothing picked yet — try the keyboard.
<UiDropdown :items="items" label="Actions" @select="…" />| Prop | Type | Default | Description |
|---|---|---|---|
| items | DropdownItem[] | — | Required. { label, value?, icon?, disabled?, danger? } |
| label | string | undefined | Trigger text and the menu’s accessible name. |
| side | 'top' | 'bottom' | 'left' | 'right' | 'bottom' | Preferred side; flips when it would run off screen. |
| align | 'start' | 'center' | 'end' | 'end' | Menus usually hang from the right edge of their trigger. |
| triggerClass | string | undefined | Extra classes for the trigger button. |
| @select | (item: DropdownItem, index: number) => void | — | Fires once, then the menu closes. |
EmptyState
What a table shows before it has rows. Say what is missing and offer the one action that fixes it — an empty screen with no way forward is a dead end.
Page and card size
No bookings yet
Sales made by your agents will show up here as soon as the first seat is paid for.
Nothing matched that search
<UiEmptyState title="No bookings yet" description="…">| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | — | Required. The one line that explains the emptiness. |
| description | string | undefined | A sentence of context below the title. |
| icon | string | 'lucide:inbox' | Decorative icon, hidden from screen readers. |
| size | 'sm' | 'md' | 'md' | sm fits inside a card, md fills a page. |
FeatureLock
Wraps a paid feature, shows it dimmed, and offers the way to unlock it. The locked content is marked inert as well as blurred: a paywall you can still reach with Tab is decoration, not a paywall.
Locked report export
Available on a higher plan
Scheduled exports are part of the Gold plan.
<UiFeatureLock :locked="plan !== 'gold'" :plans="plans">| Prop | Type | Default | Description |
|---|---|---|---|
| locked | boolean | true | Unlock by binding this to the customer’s plan. |
| title | string | 'Available on a higher plan' | Headline on the veil. |
| description | string | undefined | One sentence saying what unlocks it. |
| ctaLabel | string | 'See plans' | Button text. |
| plans | UpgradePlan[] | undefined | When given, the button opens UpgradeDialog with these. |
| @upgrade | () => void | — | Fires on every button press, dialog or not. |
| @select | (name: string) => void | — | Forwarded from the dialog. |
Loading
Pair it with useLoading, which counts running tasks per key. Two requests sharing a key both have to finish before the indicator goes away — a plain boolean would clear it as soon as the first one returned.
Overlay driven by useLoading
Manifest for departure JKT-BDG 07:00.
32 of 40 seats sold.
await run("manifest", () => fetchManifest())| Prop | Type | Default | Description |
|---|---|---|---|
| show | boolean | true | Whether the indicator is rendered. |
| variant | 'inline' | 'overlay' | 'inline' | Overlay covers the nearest positioned ancestor. |
| label | string | 'Loading' | Shown beside the spinner and read out. |
| size | 'sm' | 'md' | 'lg' | 'md' | Spinner diameter. |
Modal
Focus moves into the panel when it opens, Tab cycles inside it, and focus returns to the button that opened it on close. The page behind stops scrolling — and stays locked if a second dialog opens on top and then closes.
Dialog with footer actions
<UiModal v-model:open="open" title="Cancel booking">| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | false | Two-way, written as v-model:open. |
| title | string | undefined | Heading, also the accessible name. |
| size | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Panel width. |
| closable | boolean | true | Shows the close button. |
| dismissible | boolean | true | Escape and backdrop clicks close it. |
Popover
A floating panel that holds anything — a filter form, a summary, a colour picker. It owns its trigger button on purpose: aria-expanded and aria-controls only mean something on the control that is actually pressed. Escape and a click outside both close it; a click on the trigger closes it too, rather than closing and reopening in the same gesture.
Filter panel
<UiPopover label="Filters" align="start">| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | false | Two-way, written as v-model:open. |
| label | string | undefined | Trigger text and the panel’s accessible name. |
| side | 'top' | 'bottom' | 'left' | 'right' | 'bottom' | Preferred side; flips when it would run off screen. |
| align | 'start' | 'center' | 'end' | 'center' | Alignment along the cross axis. |
| offset | number | 8 | Gap between trigger and panel, in pixels. |
| triggerClass | string | undefined | Extra classes for the trigger button. |
| panelClass | string | undefined | Extra classes for the panel — width lives here. |
Progress
A determinate bar announces its value to screen readers; an indeterminate one deliberately does not, because claiming a number nobody knows is worse than admitting the work is open-ended.
Determinate and indeterminate
<UiProgress :value="35" label="Upload" show-value />| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | null | null | null means indeterminate. |
| max | number | 100 | Upper bound of the scale. |
| label | string | undefined | Accessible name. |
| tone | 'primary' | 'success' | 'warning' | 'danger' | 'primary' | Bar colour. |
| size | 'sm' | 'md' | 'md' | Track height. |
| showValue | boolean | false | Shows the percentage beside the track. |
Toast
Put one <UiToast /> in your layout and raise notifications from anywhere with useToast(). The queue lives at module level, so the code that raises a notification never has to reach the component that shows it. Four at a time is the ceiling — the oldest makes way.
Raise a few
The sticky error stays until you close it — that is what duration: 0 means.
const toast = useToast(); toast.success('Booking confirmed')Component props
| Prop | Type | Default | Description |
|---|---|---|---|
| position | 'top-right' | 'bottom-right' | 'top-center' | 'bottom-right' | Corner the stack grows from. |
useToast()
| Prop | Type | Default | Description |
|---|---|---|---|
| push(options) | (options: ToastOptions) => number | — | Queues a notification, returns its id. |
| success / danger / warning / info | (message, options?) => number | — | Shorthand for push with that variant. |
| dismiss(id) | (id: number) => void | — | Removes one notification early. |
| clear() | () => void | — | Empties the queue and cancels every timer. |
| duration | number | 4000 | Milliseconds before it closes itself. 0 keeps it until dismissed. |
Tooltip
A short label on hover and on keyboard focus — never on hover alone, or half your users never see it. Escape dismisses it. Put the trigger directly inside the default slot: the wrapper carries the aria-describedby that links label to control. Near a screen edge the label flips to the other side instead of being cut off — try the one pinned to the right.
Hover or focus
<UiTooltip text="Releases the seat"><UiButton>Release</UiButton></UiTooltip>| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | — | Required. The label itself. |
| side | 'top' | 'bottom' | 'left' | 'right' | 'top' | Preferred side; flips when it would run off screen. |
| align | 'start' | 'center' | 'end' | 'center' | Alignment along the cross axis. |
| delay | number | 150 | Milliseconds before it appears. 0 shows it instantly. |
| disabled | boolean | false | Never shows, without removing the wrapper. |
Upgrade dialog
The paywall a SaaS admin hits when a feature is above their plan. The plan they already pay for is shown but not selectable — hiding it would leave them guessing what they have.
Plan comparison
<UiUpgradeDialog v-model:open="open" :plans="plans" @select="pilih" />| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | false | Two-way, written as v-model:open. |
| plans | UpgradePlan[] | — | Plans to compare, in display order. |
| title | string | 'Upgrade your plan' | Dialog heading. |
| subtitle | string | 'Change or cancel whenever you like.' | Line under the heading. |