HabCard
The same surface as HabPanel, plus a classifying color: an 8px left border and a faint tint of it across the whole card.
<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
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 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.
<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.
<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
.stopon your own handler behave exactly as they would from a mouse.
Selection
selected swaps selectedAccent in for accent. On its own it changes nothing — the pair is the API.
<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
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
Applies to 4 channels
<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-visibleoutline is its focus indicator. selectedbecomesaria-pressedwhen the card is clickable, and warns when it is not.loadingsetsaria-busy; the bar itself isaria-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
| Prop | Type | Default | Description |
|---|---|---|---|
accent | string | undefined | The 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. |
as | string | "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. |
blocking | boolean | true | Whether 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. |
loading | boolean | false | Shows 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. |
saving | boolean | false | A 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. |
selected | boolean | false | Marks 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. |
selectedAccent | string | undefined | The accent swapped in while selected. |
tint | boolean | true | Fills the card with a ~6% tint of accent. Has no effect without an accent, since there is no color to tint with. |
Slots
| Slot | Bindings | Description |
|---|---|---|
default | any | Card content. |