Skip to content

HabBanner ​

A persistent, in-flow notice about the page or a form. Flat, no elevation — it belongs to the layout.

This is the info tone
Body copy sits under the title in the secondary ink.
This is the success tone
Body copy sits under the title in the secondary ink.
This is the warning tone
Body copy sits under the title in the secondary ink.
This is the error tone
Body copy sits under the title in the secondary ink.
Tinted, in-flow, no elevation — a 4px notice edge in the tone
vue
<HabBanner tone="warning" title="Sync paused">
	Retrying in 30 seconds.
</HabBanner>

Which notice ​

The three differ by placement, not by look. They share one anatomy — tone edge, full-strength tone icon, tinted surface, neutral copy:

ComponentIs
HabBannerin-flow and persistent
HabStatusStripthe standing state of a region — thicker edge, heavier title
HabToastHostelevated, fixed and transient

The edge width is the difference you can see. A banner takes the 4px --hab-edge-notice: something has happened and will stop being true. A strip takes the 8px --hab-edge-classify: this is the standing condition of the region. Using the wrong one makes the width mean two things.

Never use a banner for a decision that must be made before continuing — that is a modal.

Solid is the escalation ​

Deploy failed
The build could not be published to production.
2m ago
Read-only mode
Warning takes INK on its fill, not white — so the recession is mixed toward the fill rather than toward white.
solid is the escalation — at most one per screen

Saturation is the escalation axis, not the component. solid is reserved for a blocking or brand-level announcement, at most one per screen. It drops the edge, because the fill already carries the tone.

Recession on a solid fill is a mix toward the fill, never opacity — opacity is the alpha-text problem the solid ink ramp exists to avoid, and it compounds over a tint. Note warning takes ink on its fill rather than white, so a white wash would disappear on it; every recession and the progress track are mixed per tone for exactly that reason.

Progress and dismissal ​

Importing hosts
412 of 1030 rows processed.
40%
A determinate bar, and a dismiss affordance that only exists when you can dismiss it
vue
<HabBanner title="Importing hosts" :progress="40" @dismiss="hide">
	412 of 1030 rows processed.
</HabBanner>

progress renders a determinate bar with its value reported — unlike HabProgressBar, which is indeterminate by omitting the value. It borrows the banner's title as its accessible name; without a title, pass progress-label, and Habitus warns if you pass neither.

The dismiss affordance appears only when a @dismiss listener is attached. A banner the user cannot dismiss should not have a close button, so the listener is what decides.

An icon named at runtime ​

icon takes SVG path data, which cannot travel through JSON. Where a glyph's identity arrives from an API response, a database row, or a field a user typed into, pass your own markup through the leading slot instead — it replaces the tone glyph and keeps the tone's color. There is no name list Habitus could ship that would cover an open set.

Telemetry link degraded
The ground station reported this by name; nothing in the library knew it.
A glyph the library never carried, passed through the leading slot
vue
<HabBanner tone="error" :title="notice.title">
	<template #leading>
		<i :class="`mdi mdi-${notice.icon}`" />
	</template>
	{{ notice.message }}
</HabBanner>

That example uses the @mdi/font webfont, which resolves any of the 7,447 MDI names from a class name for the price of one stylesheet. Loading it is the host application's call; Habitus does not.

Every component with an icon prop has the same escape hatch, and this section is the one place it is explained — the per-component API tables name the slot without repeating the reasoning. It is leading everywhere except HabIconTile, which takes default: that component is nothing but the glyph, so there is nothing for an adornment to lead. HabInput also has trailing, which stays inside its button so the label and click handler survive the swap.

The quiet register ​

Your role can read this process. Attaching activities needs write access.
For comparison: the notice register, with its 4px edge and its tint.
Nothing is wrong, so nothing claims to be — no edge, no tint, no tone glyph
vue
<HabBanner tone="neutral" edge="none" :icon="mdiLockOutline">
	Your role can read this process. Attaching activities needs write access.
</HabBanner>

tone="neutral" with edge="none" is the bar for content where nothing is wrong — a read-only strip, a "this is how this screen works" line. Banner's 4px notice edge says something has happened; HabStatusStrip's 8px edge says this is the standing condition of the region. Both overstate a sentence that is merely true, which is why every read-only surface in the product was inventing its own bar.

It ships no default glyph. The other four tones each have one that names a state, and there is no glyph that means "no state" — defaulting to the info mark would put a claim on the one bar whose job is not to make one. Pass the glyph that says what this bar is about: a lock, a clock, an eye.

edge is orthogonal to tone, so any tone can drop its edge. neutral has no tone color to draw one with, so its left border stays the shell's own hairline either way.

Accessibility ​

  • Give it a title, a body, or both. The tone's color and glyph repeat the meaning; they must not be the only carrier of it.
  • The icon is decorative and aria-hidden. Pass :icon="null" to drop it where the title already says everything.
  • Markup you pass through leading is yours to hide. Habitus sets aria-hidden on the glyph it renders, but not on the slot: the same slot is where a HabAvatar or a HabCheckbox goes, and hiding those would be wrong. So a decorative glyph of your own needs its own aria-hidden="true".
  • The dismiss button carries aria-label, from dismiss-label. There is no default — the string is yours to supply, and an @dismiss handler without it warns.
  • A banner that appears in response to something is not announced by default. This renders static markup; if it arrives asynchronously, put it in a live region, or use a toast, which is built for exactly that.
  • Every tone's tint/text and fill/ink pair is contrast-measured. The mixes for recession are derived from those, so they inherit the measurement.

API ​

Props ​

PropTypeDefaultDescription
dismissLabelstringundefinedAccessible name for the dismiss button. Required on any banner with an @dismiss handler: Habitus ships no default, so the string is the host application's to translate.
edge"notice" | "none""notice"Which left edge the banner draws, if any.
iconstring | nullundefinedSVG path data, e.g. mdiAlert from @mdi/js. Defaults per tone; pass null to drop it.
notestringundefinedSmall right-aligned line — a timestamp, or an AI-output disclaimer.
progressnumberundefined0–100. Renders a determinate progress bar, for a countdown or a job the banner is reporting on.
progressLabelstringundefinedAccessible name for the progress bar, when there is no title to borrow. A bar that reports a value with no indication of what it measures is announced as a bare number.
titlestringundefined—
tone"error" | "neutral" | "info" | "success" | "warning""info"neutral is the quiet register: no tint, no tone glyph, and nothing claiming anything is wrong. Pair it with edge="none" for a read-only or how-this-works strip. It ships no default icon — there is no glyph that means "neutral", so pass the one that means this (a lock, a clock).
variant"tinted" | "solid""tinted"—

Slots ​

SlotBindingsDescription
actionanyOne trailing affordance — a text button or a link.
defaultanyBody copy.
leadinganyLeading adornment, replacing the glyph. Takes precedence over icon.

Proprietary — internal use only.