Skip to content

HabCard ​

The same surface as HabPanel, plus a classifying color: an 8px left border and a faint tint of it across the whole card.

No accent — the same surface as a panel
Accent, with the 6% tint it implies
Plain, then classified — the border is always there, the accent replaces its left edge
vue
<HabCard accent="var(--hab-info)">…</HabCard>

Reach for HabPanel when the surface is just a container. Reach for HabCard when the color means something — the border says the same thing in both, and the card adds the fill.

HabCard has no padding of its own. It is a surface for rows and tiles that bring their own layout, which is also why its content examples here carry their own px-5 py-4.

The accent classifies ​

Incident
Maintenance
Resolved
Note
The accent tells you what kind of thing the row is

It tells the reader what kind of thing this is — an entity kind, a session role, an incident severity. Any CSS color works, including one that arrives with the data.

As with HabPanel, the width is the meaning and it is shared across the system: --hab-edge-notice (4px) is a transient notice, --hab-edge-classify (8px) is a standing or classifying edge. It is not an emphasis dial.

The 1px border stays on the other three sides. Without it a card with no accent is --hab-surface on a tinted page, with nothing to separate it from the background.

Tint ​

tint (default)
tint=false
tint=false keeps the classifying edge and drops the wash

tint defaults to true and mixes a ~6% wash of the accent into the surface. It has no effect without an accent, since there is no color to tint with.

vue
<HabCard accent="var(--hab-error)" :tint="false">
	…
</HabCard>

Making a card clickable ​

Attach a @click. That is all — the card takes role="button", a tab stop, a pointer cursor and an Enter/Space handler along with it.

Opened 0 times
Tab to it and press Enter or Space — a @click alone brings the whole keyboard contract
vue
<HabCard @click="open(session)">…</HabCard>

There is no interactive prop, on purpose. The design system records this as a real defect it shipped once: a card that received its handler through a props spread got the click and none of the role, the tab stop or the key handler — a dead end for anyone not using a mouse. Vue's attribute fallthrough is that spread, so a separate opt-in prop would leave the same footgun one forgotten prop away. Deriving it from the listener makes the guarantee unconditional.

Two consequences worth knowing:

  • Do not put other controls inside a clickable card. role="button" cannot contain a button or a link — the nesting is invalid, and the inner control stops being reliably reachable. If a row needs both a primary action and its own buttons, leave the card inert and make the primary action a real control inside it.
  • Enter and Space dispatch a genuine click, so modifiers and .stop on your own handler behave exactly as they would from a mouse.

Selection ​

Morning standup
Design review
Retrospective
selected swaps the accent, and reports aria-pressed to a screen reader

selected swaps selectedAccent in for accent. On its own it changes nothing — the pair is the API.

vue
<HabCard
	accent="var(--hab-border-strong)"
	selected-accent="var(--hab-accent)"
	:selected="chosen === session"
	@click="chosen = session"
>
	…
</HabCard>

On a clickable card the state is reported as aria-pressed. On a non-clickable card there is no role to carry it, so the selection would be visible to the eye and invisible to a screen reader — Habitus warns in the console rather than shipping that silently.

Loading ​

Syncing hosts…
No accent — the bar falls back to the accent token
A 3px indeterminate bar, and aria-busy. Under reduced motion it fills the track instead of moving.

A 3px indeterminate bar across the top, plus aria-busy on the card. The bar takes its color from the accent, falling back to --hab-accent.

Under prefers-reduced-motion the bar stops moving and fills the track instead of disappearing: an indeterminate bar that cannot move conveys nothing, so the state stays legible and only the movement goes.

Saving ​

Policy

Applies to 4 channels

saving is the card's record going OUT — the wave. loading is data coming IN — the bar. They read differently on purpose.
vue
<HabCard :saving="saving">…</HabCard>

saving is the card's own record being written out: a wave over the whole card. loading is data coming in: the bar across the top. A card can be both, and the two read differently on purpose. Either one sets aria-busy.

blocking is on by default, and makes the card inert — a clickable card stops taking clicks and focus until the write lands. The wave is the same one HabSavingFrame draws; its page has the rules.

Accessibility ​

  • Adds no semantics at all unless it is clickable — correct for a surface.
  • A clickable card is a role="button" with a tab stop and an Enter/Space handler, and the native :focus-visible outline is its focus indicator.
  • selected becomes aria-pressed when the card is clickable, and warns when it is not.
  • loading sets aria-busy; the bar itself is aria-hidden, since the state is already carried.
  • The accent color is decorative. Never let it be the only thing that carries meaning — a color-blind reader, or anyone in forced-colors mode, needs the kind stated in the content too. As a border it also needs 3:1 contrast against the surface behind it, which is your responsibility when the color is arbitrary.
  • A clickable card needs an accessible name from its content. If the content is only an icon or a color, give the card an aria-label.

API ​

Props ​

PropTypeDefaultDescription
accentstringundefinedThe 8px colored left border, and the source of the tint. Any CSS color, including one that comes from data — the 6% fill is mixed with color-mix(), so var(--hab-info) works. It classifies: it tells the reader what kind of thing this is. Omit it for the plain form — --hab-surface with a 1px border and no left edge. As a border it needs a 3:1 contrast ratio against the surface behind it.
asstring"div"Element or component to render as. Note a clickable card takes role="button", which replaces whatever semantics as supplied — so pair as="li" with a non-clickable card, or put the li around it.
blockingbooleantrueWhether a saving wave also swallows input: a veil washes the card out and it goes inert — a clickable card stops taking clicks and focus. On by default, because the write is against everything the card shows.
loadingbooleanfalseShows an indeterminate bar across the top and sets aria-busy. Under prefers-reduced-motion the bar stops moving and fills the width, so the state is still legible without the animation.
savingbooleanfalseA write against the card's own record is in flight: a wave sweeps the whole card and it is marked aria-busy. Not loading, which is data coming in and draws a bar. This is the record going out. A card can be both, and the two read differently on purpose.
selectedbooleanfalseMarks the card selected. Only has a visible effect together with selectedAccent, which is the color it swaps in. On a clickable card this also sets aria-pressed, so the state reaches assistive technology. On a non-clickable card there is no role to carry that, and Habitus warns instead of shipping a selection only sighted users can perceive.
selectedAccentstringundefinedThe accent swapped in while selected.
tintbooleantrueFills the card with a ~6% tint of accent. Has no effect without an accent, since there is no color to tint with.

Slots ​

SlotBindingsDescription
defaultanyCard content.

Proprietary — internal use only.