Skip to content

HabSkeleton ​

Waiting states. Four components, because "waiting" is four different situations and the wrong one misleads.

UseWhen
HabSkeletoncontent whose shape you already know is loading
HabSpinneran action is in flight
HabProgressBarwork is attached to a region, not to a control
HabEmptyStateit finished, and there is nothing there

Skeleton ​

Match the real layout — a skeleton that does not is worse than a spinner
vue
<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 ​

Syncing hosts…Publishing
A spinner for an action in flight; a bar for work attached to a region
vue
<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 ​

vue
<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 ​

No hosts match
Try a shorter search term, or clear the status filter.
A prompt, not an apology — say what belongs here and give the one action
vue
<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 ​

  • HabSkeleton is aria-hidden. It has no text to announce, and reading out its shape is noise. Put aria-busy on the container that is filling, so assistive technology knows the region is not finished.
  • HabSpinner is a role="status", so its label is announced when it appears and announced politely rather than interrupting.
  • HabProgressBar carries no aria-valuenow, and that absence is what marks it indeterminate. A value of 0 would claim no progress instead.
  • HabEmptyState renders 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 ​

PropTypeDefaultDescription
countnumber1Number 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.
heightnumber12Bar height in px.
radiusstringundefinedOverride the radius — 50% for an avatar placeholder.
widthstring"100%"Any CSS width.

HabSpinner ​

Props ​

PropTypeDefaultDescription
colorstringundefinedThe glyph's color. Any CSS color; defaults to the accent.
labelstringundefinedText 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 ​

PropTypeDefaultDescription
labelstring—Accessible name. Say what is loading, not that something is.

HabEmptyState ​

Props ​

PropTypeDefaultDescription
iconstringundefinedSVG path data from @mdi/js. Defaults to the empty-inbox glyph.
notestringundefinedOne sentence: what belongs here, and why it is empty.
titlestringundefined—

Slots ​

SlotBindingsDescription
actionanyThe single action that fills it.
leadinganyLeading adornment, replacing the glyph. Takes precedence over icon.

Proprietary — internal use only.