HabPanel
The neutral container. Every block of content sits in one: one border, one radius, no shadow.
Every block of content sits in one of these.
<HabPanel>
<p>Content</p>
</HabPanel>Reach for HabPanel when the surface is just a container. Use HabCard instead when the whole surface should carry a tint of a classifying color — the border means the same thing in both, and the card adds the fill.
Header and footer
Both are slots, and each brings its own divider. The footer sits on --hab-surface-2 and right-aligns its children, so a row of buttons needs no layout of its own.
Choose which events send you an email.
<HabPanel>
<template #header>
<strong>Notification settings</strong>
</template>
<p>Choose which events send you an email.</p>
<template #footer>
<HabButton
variant="text"
tone="neutral"
>
Cancel
</HabButton>
<HabButton>Save</HabButton>
</template>
</HabPanel>An empty slot renders nothing at all rather than an empty band — a divider with no content above it reads as a stray line.
Header and footer padding is fixed and does not track the padding prop. They are chrome; only the body scales.
Padding
Small
Medium
Large
Three steps, all on the 4px grid. md is the default at 16px 20px — asymmetric on purpose, because the vertical rhythm is tighter than the horizontal gutter.
The design system specifies a free-form CSS string here. Habitus narrows it to a closed scale, because the same design system requires internal padding to sit on the grid, and a free-form string is an open door to off-grid values. Recorded in design-source.md.
Flush
flush drops body padding entirely, for content that manages its own — a table, or a list whose rows need to reach the panel edge to show their full-width hover and dividers.
- api.livereader.com
- cdn.livereader.com
- status.livereader.com
<HabPanel flush>
<ul class="divide-y divide-divider">
<li class="px-5 py-3">api.livereader.com</li>
</ul>
</HabPanel>flush wins over padding, so the two are never in conflict.
It is the same word HabAccordion and HabInspectorPanel use for the same idea.
The accent edge
accent adds an 8px colored left border. It classifies — it says what kind of thing this is.
<HabPanel accent="var(--hab-error)">
<strong>Incident</strong>
</HabPanel>It is not an emphasis dial. The width is the meaning, and it is shared across the system: --hab-edge-notice (4px) marks a transient, in-flow notice; --hab-edge-classify (8px) marks a standing or classifying edge, identically on HabPanel, HabCard and HabStatusStrip. Using the width to mean "more important" makes it mean two different things depending on which component you picked.
The accent replaces the left edge only; the 1px border stays on the other three sides. Without it, a panel on a tinted page has nothing separating it from the background.
Any CSS color works, including one that comes from data. As a border it needs a 3:1 contrast ratio against the surface behind it — that is your responsibility when the color is arbitrary.
Saving
<HabPanel :saving="saving">…</HabPanel>saving is for a panel that saves as a unit — a settings block, the form it holds. The wave covers the whole panel, header and footer included, and blocking (on by default) washes it out and makes it inert until the write lands.
A write against one row inside the panel reports itself on the row, not here. The wave is the same one HabSavingFrame draws; its page has the rules.
Accessibility
Renders a plain
<div>by default, which adds no semantics — correct for a generic container.For a landmark, use
as="section"with a label:vue<HabPanel as="section" aria-labelledby="hosts-title"> <template #header> <h2 id="hosts-title">Hosts</h2> </template> </HabPanel>A
<section>with no accessible name is not exposed as a region at all, so the pairing is the whole point. Do not reach for it on every panel; a page of unnamed regions is noise, and too many landmarks is as unhelpful as none.The panel does not create a heading level. If the header holds a heading, it is yours to place correctly in the document outline.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
accent | string | undefined | Adds an 8px colored left border. Any CSS color. It classifies — it says what kind of thing this is. It is not an emphasis dial: 4px is a notice, 8px is a standing or classifying edge, and the width carries that meaning identically on HabCard. As a border it needs a 3:1 contrast ratio against the surface behind it. |
as | string | "div" | Element or component to render as. Use section together with aria-labelledby when the panel should be a landmark. |
blocking | boolean | true | Whether a saving wave also swallows input: a veil washes the panel out and it goes inert. On by default, because the write is against everything the panel shows. |
flush | boolean | false | Drops body padding entirely, for tables and lists that manage their own. Takes precedence over padding. The same word as HabAccordion's and HabInspectorPanel's flush, and the same idea: the container stops padding so its rows can reach the edge. |
padding | "md" | "sm" | "lg" | "md" | Body padding. md is 16px 20px. |
saving | boolean | false | A write against the whole panel's subject is in flight — a settings block that saves as a unit. A wave sweeps the panel, header and footer included, and it is marked aria-busy. Not for a write against one row inside it: a row reports itself on the row. Writing out, not reading in — that is a HabSkeleton in the body. |
Slots
| Slot | Bindings | Description |
|---|---|---|
default | any | Panel body. |
footer | any | Action bar below a divider, on --hab-surface-2, right-aligned. |
header | any | Title area above a divider — usually a section heading. |