Skip to main content
Copy for LLM

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

A promo bar
Summer sale — 20% off every plan through Friday.

With an action

An inline call to action
WireKit v2 is out — chat components, mockups, and a lot more. Read the changelog

Intents

promo is the marketing default; the state intents carry their usual meaning for a notice.

Intents
Promo — 20% off every plan.
Info — a new region is available.
Warning — maintenance on Sunday, 02:00–04:00 UTC.
Danger — the API v1 shutdown is in 7 days.

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.

Solid intents, and two hues
Danger — solid.
Develop — hue 250.
Staging — hue 300.

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 dismissKey and 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):

Dismissible
Summer sale — 20% off every plan through Friday. See pricing

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.

An environment line on develop
Develop — this is not production
{{-- 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.

A sticky bar inside its own scroller

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.

Full-width bar with a centered message and action
A new region is available — deploy closer to your users in eu-central. See regions

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-label or aria-labelledby of yours names the banner and makes it a region, as label does, 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.

  • label is what makes it a landmark, and it has no default. Give it one and the bar becomes a named role="region" a reader can find and skip; leave it out and it is ordinary content.

    That is deliberate rather than an omission. position accepts top and bottom, 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 is aria-hidden.

  • x-cloak keeps 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

Further Reading

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store