Banner
A full-width, solid-intent announcement bar with an optional dismiss button and top or bottom pinning. Made for page-level messages: maintenance notices, promos, and outage warnings.
Import
import { Banner } from "@hyzer-labs/ui"Demo
Banner vs Alert
Banner for a solid, full-width announcement at page level: maintenance notices,
promos, outage bars. You can pin it to an edge. When the message is inline and sits next to the
thing it describes, use Alert instead.The intent vocabulary, using a solid fill instead of the tint Alert uses.
<Banner>Course note: hole 7 tee pads were resurfaced this week.</Banner>
<Banner intent="primary">League night: tags round starts at 5:30pm on Thursday.</Banner>
<Banner intent="secondary">Doubles signup: random-draw doubles opens 30 minutes early.</Banner>
<Banner intent="danger">Course closed: lightning in the area. Clear the course now.</Banner>
<Banner intent="warning">High winds: gusts over 30mph expected after 2pm.</Banner>
<Banner intent="success">Round saved: your scorecard was synced to the league.</Banner>
<Banner intent="info">Cart friendly: the back nine is paved end to end.</Banner>Same contract as Alert and Badge: onDismiss renders the button, you own the
visibility state. Give the button a specific dismissLabel.
let noticeVisible = $state(true);
{#if noticeVisible}
<Banner
intent="warning"
dismissLabel="Dismiss maintenance notice"
onDismiss={() => (noticeVisible = false)}
>
Scheduled maintenance tonight from 11pm–1am.
</Banner>
{/if}The actions snippet is a trailing slot for a Link or Button. Both are retargeted onto the bar's own foreground/background pair,
so they stay legible against the intent fill.
<Banner intent="primary">
New season pricing is live.
{#snippet actions()}<Link href="#">Learn more</Link>{/snippet}
</Banner>children is a plain snippet, so any markup is allowed. For a
heading-and-body bar, put the opt-in hz-banner-title theme class on your
own lead element, the same convention as hz-card-title. The class goes
block-level, so the lead stacks over the body copy with no wrapper markup, while the
icon, actions, and dismiss cells keep their places on the row.
<Banner intent="info">
<strong class="hz-banner-title">Winter league registration is open</strong>
Sign up by November 1. Rounds move to 10am starts after the time change.
{#snippet actions()}<Link href='#'>Register</Link>{/snippet}
</Banner>pin="top" and pin="bottom" stick the bar to that edge of its scroll
container, in flow, so nothing beneath it is overlapped. Scroll the box below to see it stay
put.
Keep pinned banners short. A bar that grows tall can cover a focused element that
scrolls under it (WCAG 2.4.11). Give in-page anchor targets a scroll-margin-block-start/-end equal to the banner height so they clear it.
For viewport-fixed behavior instead of in-flow sticky, pass your own class with position: fixed. That is a CSS override rather than a prop.
Scroll to see the banner stay pinned to the top of this box…
It never overlaps this text: sticky keeps it in flow.
Keep scrolling.
Still here.
The bottom of the scrollable region.
<div class="scroll-wrap">
<Banner pin="top" intent="danger">Course closed: lightning in the area.</Banner>
<!-- …scrollable page content… -->
</div>
/* your CSS */
.scroll-wrap { max-height: 12rem; overflow-y: auto; }Persisting a dismissal is your logic. Banner only fires onDismiss: it keeps
no dismissed state of its own and ships no localStorage handling. A typical pattern:
let dismissed = $state(localStorage.getItem('promo-dismissed') === 'true');
function dismiss() {
dismissed = true;
localStorage.setItem('promo-dismissed', 'true');
}
{#if !dismissed}
<Banner intent="primary" onDismiss={dismiss}>
Free shipping this weekend only.
</Banner>
{/if}Props
| Name | Type | Default | Note |
|---|---|---|---|
children | Snippet | — | Required. The banner body. |
intent | Intent | 'neutral' | See Foundation → Colors & Intent. |
pin | 'top' | 'bottom' | — | Sticks the bar to that edge, in flow (position: sticky). Static by default. |
icon | Snippet | — | Decorative; rendered aria-hidden. |
actions | Snippet | — | Trailing slot — a Link or Button, e.g. "Learn more". |
onDismiss | () => void | — | Renders the dismiss button. Visibility is your state — the Banner never hides itself. |
dismissLabel | string | 'Dismiss' | |
as | string | 'div' | Polymorphic root — e.g. section or aside for landmark semantics. |
class | string | — | Merged after the hz-banner class. |
Theme hooks
What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.
Root class: .hz-banner
Data attributes
| Hook | Values | Styles |
|---|---|---|
data-intent | 'neutral' | any registered intent | Drives the solid fill via --hz-banner-bg; spans the intent registry. |
data-pin | 'top' | 'bottom' | Present only when pinned; sticks the bar to that edge via position: sticky. |
data-dismissible | present when dismissible | Present only when onDismiss is set — target it to style the dismissible form. |
Custom properties
| Hook | Values | Styles |
|---|---|---|
--hz-banner-bg | <color> — default var(--hz-intent-neutral) | The solid fill, switched per intent — override to restyle every intent's bar. |
--hz-banner-fg | <color> — default var(--hz-color-surface) | On-fill text colour, the surface role so it flips with the mode and keeps text ≥ 4.5:1 in both. |
--hz-banner-padding-block | <length> — default 1.5rem | Vertical padding of the bar. |
--hz-banner-padding-inline | <length> — default 2.5rem | Horizontal padding of the bar. |
Part classes
| Hook | Values | Styles |
|---|---|---|
.hz-banner-icon | child element | The decorative leading icon. |
.hz-banner-content | child element | The message body. |
.hz-banner-actions | child element | The trailing actions slot. |
.hz-banner-dismiss | child element | The dismiss button. |
.hz-banner-title | opt-in class | Banner never emits this — put it on your own lead element (strong, a heading) and it goes block-level semibold, stacking over the body copy. The .hz-card-title convention. |
Accessibility
A statically rendered Banner is plain content: no role, no live region. For a Banner inserted after load, pass role="status" (polite) or role="alert" (assertive, use sparingly) via the rest props.
The icon slot is decorative (aria-hidden). Links inside are underlined, and color is never the only signal. The dismiss button is a real labeled <button>, and dismissal is your state change.
A pinned Banner can cover a focused element that scrolls under it. Keep pinned banners short, one line where possible. Give in-page anchor targets scroll-margin-block-start/-end equal to the banner height so they clear it. The Banner itself sets no scroll-margin.
There is deliberately no Toast component. A timed self-dismissing overlay is hard to make accessible. Under WCAG 2.2.1 the timing has to be adjustable, extendable, or pausable. A message that vanishes on its own often goes unannounced as well. The library prefers dismissal the reader chooses, so content never disappears out from under them. Banner is the supported pattern for a pinned or site-wide message instead, with opt-in role="status" and dismissal you own.
References: WCAG 2.4.11: Focus Not Obscured (Minimum) · MDN: status role · MDN: alert role