HabSkeleton
Waiting states. Four components, because "waiting" is four different situations and the wrong one misleads.
| Use | When |
|---|---|
HabSkeleton | content whose shape you already know is loading |
HabSpinner | an action is in flight |
HabProgressBar | work is attached to a region, not to a control |
HabEmptyState | it finished, and there is nothing there |
Skeleton
<HabSkeleton width="2rem" :height="32" radius="50%" />
<HabSkeleton :count="2" />Match the real layout. A skeleton that does not is worse than a spinner: it promises a shape, and the content then arrives in a different one, which reads as the page breaking.
count stacks bars and shortens the last to 62%, as a paragraph's last line is — a stack of identical bars reads as a table rather than as prose.
Spinner
<HabSpinner label="Syncing hosts…" />The system's one spinning mark. Give it a label and say what is happening — it is also the spinner's accessible name, and "Loading" on its own tells nobody what for.
For page content whose shape is known, reach for HabSkeleton: a spinner says "wait", a skeleton says "wait, and here is what for".
ProgressBar
<HabProgressBar label="Loading hosts" />A 3px indeterminate bar for work attached to a region — the top of a panel, under a toolbar — where a floating glyph has nothing to anchor to.
It is not the same bar HabCard's loading draws. That one tints the card's own accent at 20% and has no radius, because it is an edge of a surface. This one sits on --hab-accent-tonal with its own radius, because it is a standalone element.
Under prefers-reduced-motion it fills the track rather than freezing a sliver — an indeterminate bar that cannot move conveys nothing.
EmptyState
<HabEmptyState
:icon="mdiMagnify"
title="No hosts match"
note="Try a shorter search term, or clear the status filter."
>
<template #action>
<HabButton
variant="outlined"
tone="neutral"
>Clear filters</HabButton
>
</template>
</HabEmptyState>A prompt, not an apology. Say what belongs here and why it is empty, then give the single action that fills it. "No results" alone leaves the reader with nothing to do.
The note's measure is capped at 380px and balanced with text-pretty: centred prose is unreadable much past that, and a two-line note should not leave one orphaned word.
Accessibility
HabSkeletonisaria-hidden. It has no text to announce, and reading out its shape is noise. Putaria-busyon the container that is filling, so assistive technology knows the region is not finished.HabSpinneris arole="status", so its label is announced when it appears and announced politely rather than interrupting.HabProgressBarcarries noaria-valuenow, and that absence is what marks it indeterminate. A value of0would claim no progress instead.HabEmptyStaterenders no heading element — its title is styled text, so it does not inject a level into your outline. If the empty state is the page, give it a real heading around it.
API
HabSkeleton
Props
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | 1 | Number of stacked bars. The last is shortened to 62%, as a paragraph's last line is — a stack of identical bars reads as a table, not as prose. |
height | number | 12 | Bar height in px. |
radius | string | undefined | Override the radius — 50% for an avatar placeholder. |
width | string | "100%" | Any CSS width. |
HabSpinner
Props
| Prop | Type | Default | Description |
|---|---|---|---|
color | string | undefined | The glyph's color. Any CSS color; defaults to the accent. |
label | string | undefined | Text beside the glyph. Say what is happening — it is also the spinner's accessible name, and "Loading" on its own tells nobody what for. |
size | "md" | "sm" | "lg" | "md" | — |
HabProgressBar
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Accessible name. Say what is loading, not that something is. |
HabEmptyState
Props
| Prop | Type | Default | Description |
|---|---|---|---|
icon | string | undefined | SVG path data from @mdi/js. Defaults to the empty-inbox glyph. |
note | string | undefined | One sentence: what belongs here, and why it is empty. |
title | string | undefined | — |
Slots
| Slot | Bindings | Description |
|---|---|---|
action | any | The single action that fills it. |
leading | any | Leading adornment, replacing the glyph. Takes precedence over icon. |