Announcement Banner
The full-width bar at the very top of a page: a launch, a sale, a maintenance window. It sits on the content-edge spine, so its text lines up with the rest of your page chrome instead of floating on its own margin.
It is not an Alert — an alert is about the thing the user is doing right now; an announcement is ambient, site-wide, and dismissible.
Basic Usage
With an action
Intents
promo is the marketing default; the state intents carry their usual meaning
for a notice.
Solid, and a color from a hue
surface="solid" fills the bar with the intent's color and its own contrast-paired text — the
same two treatments badge has, under the same names. soft, the default, is the tint above.
hue takes the color from a hue instead of an intent, in degrees: 250 is blue, 300 violet.
It is for a color that identifies something rather than reporting a state — which environment
this is — where warning on staging would read as a warning about something that is not one.
It does not depend on your theme's accent, which is monochrome by default.
A hue's lightness and chroma are fixed by --wk-banner-hue-l and --wk-banner-hue-c, because
they are what decides contrast, and a hue must not be able to break it. At the shipped values the
text clears AA at every hue from 0 to 360 — the weakest is around 190°, a cyan, at 5.5:1. A higher
lightness does not: at 0.55 the same cyan falls to about 4.2:1. Change the two together or not at all.
hue="theme" switches the hue tone on without writing a value, so the hue comes from your
stylesheet or your theme:
{{-- 1. The hue lives in CSS: one place for every environment, and it survives a policy that strips inline styles --}}
<x-wirekit::announcement-banner strip hue="theme" surface="solid" class="env-staging">Staging</x-wirekit::announcement-banner>
/* 2. Set it where your environment is known */
.env-staging { --wk-banner-hue: 300; }
Without either, it follows --theme-hue where a preset sets one, and falls back to 264 (indigo)
so a bar never paints transparent.
Dismissing
A dismissible bar gets a close button. There are two modes:
- Remembered (default) — give a
dismissKeyand the dismissal sticks across page loads. This is what production wants: the reader clicks the X once and the bar stays gone. - Session-only — pass
:persist="false"for a notice that reappears next visit. It needs no key, and because nothing is stored, a re-mount brings it back — which is exactly why the demo below can be dismissed and restored.
The preview is session-only, so you can click the close button and bring the bar back with the preview's replay control (or a reload):
In production you usually want it remembered. Give it a dismissKey, and
change that key when the message changes — a new announcement should not inherit
the old one's dismissal:
{{-- 1. The key IS the identity of the announcement — and it remembers --}}
<x-wirekit::announcement-banner dismiss-key="launch-v2" label="v2 launch">
WireKit v2 is out.
</x-wirekit::announcement-banner>
A close button that forgets is worse than none — the user clicks it, the bar
comes back on the next page, and the site feels broken. That is why a bar with
neither a dismissKey nor :persist="false" has no close button at all:
forgetfulness has to be a deliberate choice.
A bar that must never close
That same rule has a side you can walk into: :persist="false" on its own adds a close button,
because a session-only bar is dismissible. Turning persistence off to be safe gives a notice the
reader can click away. Say it outright instead:
{{-- 1. dismissible="false" means never, whatever dismissKey and persist say --}}
<x-wirekit::announcement-banner :persist="false" :dismissible="false">
Your account is in read-only mode until the migration finishes.
</x-wirekit::announcement-banner>
Left out, dismissible follows the rule above: a close button with a dismissKey or
:persist="false", none without either. A strip is never dismissible regardless.
Sticky
sticky keeps the bar in view while the page scrolls. It is opt-in on purpose:
a bar that follows the reader is a much stronger claim on the viewport than most
announcements have earned.
At either edge, an element that takes the focus, or that a fragment link brings into view, comes to rest beside the sticky bar rather than under it (keeping the focus clear of a pinned bar). A strip needs none of this: its height is a token the page reads.
{{-- 1. Reserve sticky for something genuinely time-critical --}}
<x-wirekit::announcement-banner sticky intent="danger" dismiss-key="api-v1-eol">
The API v1 shutdown is in 7 days.
</x-wirekit::announcement-banner>
Combine it with position="bottom" to dock a persistent notice to the foot of the
page (where a thumb reaches on a phone) instead of the top.
A strip above the page
strip turns the bar into a line above the application — pinned to the top of the viewport,
one line tall, never dismissible. It is made for the line that says which environment you are
on, and it moves the application down by exactly its own height without changing anything else
about it, whichever shell the page uses.
{{-- 1. Before the shell, anywhere at the top of the page --}}
<x-wirekit::announcement-banner strip hue="250" surface="solid">
Develop — this is not production
</x-wirekit::announcement-banner>
{{-- 2. The shell gives back the strip's height on its own --}}
<x-wirekit::app-shell viewport>…</x-wirekit::app-shell>
A strip's action fits its one line. The usual one is an inline link component,
and for an action that posts, such as ending an impersonation, the same link with as="button"
inside a form. A button component works too: in a strip it takes the line's height
whatever its size, and on a touch screen its 44px hit area reaches past the line as an invisible
patch instead of making the drawn button taller.
{{-- 1. An impersonation line whose action posts --}}
<x-wirekit::announcement-banner strip hue="30" surface="solid">
Impersonating Ada Lovelace
<x-slot:action>
<form method="POST" action="/impersonation/stop">
@csrf
<x-wirekit::link as="button" type="submit">Stop</x-wirekit::link>
</form>
</x-slot:action>
</x-wirekit::announcement-banner>
The strip writes its height to --wk-strip-inset on the page. Everything that pins itself to
the top of the viewport or takes the viewport's height folds that in: app-shell shortens itself
by it in both modes, and a sticky navbar, header, shell-bar, brand-bar, toolbar,
page-header or ordinary sticky banner sticks below the strip rather than under it. page-progress, the top
bar of reading-progress and a toast region at the top run along the application, directly
under the strip. An overlay — a modal, a drawer — covers the strip along with the page. Two strips stack —
the environment line and a maintenance notice at once — and the inset is both together.
One line, on purpose. A strip whose height a script had to measure would let the page jump once it ran; a fixed height lets the stylesheet know it before anything paints, which is what makes "changes nothing" true from the first frame. Text that does not fit is cut on screen and stays whole for a screen reader. An announcement that needs several lines belongs in an ordinary banner, which flows with the page.
Do not put a sticky navbar or toolbar inside an element that scrolls on its own while a strip
is present. The inset is measured from the top of the viewport, so inside an inner scroller that
bar would stick one strip-height below that scroller's top. Page bars belong at the page level,
where the inset is the right distance.
Full width and centered content
The bar always spans the full page width — a promo covering half the header reads as a mistake — and its content is centered, so a short message sits in the middle of the bar however wide the screen gets. That is the whole layout: a full-bleed strip with a centered notice.
For a bar whose message and controls sit at opposite edges of a content column (a navigation-style layout, not a centered notice), reach for navbar instead.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
position |
string | 'top' |
top or bottom |
sticky |
bool | false |
Keep the bar in view while scrolling |
strip |
bool | false |
A one-line strip above the page that moves the application down by its height. Implies sticky and position="top", never dismissible |
intent |
string | 'promo' |
promo, info, success, warning, danger |
surface |
string | 'soft' |
soft (tinted) or solid (filled with the intent's color and its paired text) |
hue |
int|string|null | null |
A color from a hue, 0–360, instead of an intent — or theme to read --wk-banner-hue / --theme-hue from your CSS |
dismissKey |
string|null | null |
Stable key; enables the close button and persists the dismissal across page loads |
persist |
bool | true |
Whether a dismissal is remembered across page loads. Set false for a session-only bar (also enables the close button without a key) |
dismissible |
bool|null | null |
Whether a close button exists, stated. null derives it from dismissKey and persist; false means never, whatever those two say |
label |
string | null |
Accessible name. Supplying it is what makes the bar a landmark — see Accessibility |
scope |
string|null | null |
Scoped personalization name |
Slots
| Slot | Description |
|---|---|
| default | The message |
action |
An inline link or a button. In a strip a button takes the line's height, see A strip above the page |
Accessibility
-
An
aria-labeloraria-labelledbyof yours names the banner and makes it aregion, aslabeldoes, and the dismiss button says which banner it closes. -
Never
role="alert". An alert interrupts a screen-reader user mid-sentence — the wrong trade for a promo. This holds whether or not the bar is a landmark. -
labelis what makes it a landmark, and it has no default. Give it one and the bar becomes a namedrole="region"a reader can find and skip; leave it out and it is ordinary content.That is deliberate rather than an omission.
positionacceptstopandbottom, so a promo bar above and a notice below is a first-class composition — and with a built-in default name it produced two regions both called "Announcement". Someone paging through landmarks meets two with one name and cannot tell them apart. A component may not invent the name that turns itself into a landmark; the caller chooses it, or there is none. -
The close control is a real
<button>with a real name ("Dismiss Summer sale"), never a bare glyph — and it keeps a name even on an unnamed bar, falling back to the generic word. Closing it from the keyboard moves the focus to the next control after the bar, or the last one before it, instead of leaving it on the page. The X itself isaria-hidden. -
x-cloakkeeps a previously-dismissed bar from flashing on every page load before the stored state is read.
Keyboard Interaction
| Key | Action |
|---|---|
| Tab | Focus the action, then the close button |
| Enter / Space | Activate the focused control |
Pitfalls
- Do not use it for the thing the user is doing. That is Alert.
- Do not stack banners. Two bars at the top of a page is one too many; the second one is never read.
- Do not reuse a dismiss key for a new message. The new announcement would arrive already dismissed.
- Do not make it sticky by default. Ambient news does not deserve permanent viewport.
Design Tokens
| Element | Token |
|---|---|
| Content-edge spine | --padding-wk-x-lg |
| Vertical padding, the same as the sides (a strip keeps its fixed height instead) | --padding-wk-x-lg |
promo surface |
--color-wk-accent / --color-wk-accent-fg |
| State surfaces | --color-wk-{info,success,warning,danger} mixed 12% over --color-wk-bg-elevated |
| Text size | --text-wk-sm |
| Focus ring | --color-wk-ring |
solid surface |
--color-wk-{info,success,warning,danger} / their -fg pair |
hue tone |
oklch(var(--wk-banner-hue-l) var(--wk-banner-hue-c) var(--wk-banner-hue)), text --color-wk-banner-hue-fg |
| Strip height | --wk-strip-height |
| What a strip takes from the viewport | --wk-strip-inset — read by every surface pinned to the viewport's top |