Banner
A page- or section-level announcement surface. Two layouts — the promotional panel (overline, headline, body, CTAs, media) and the thin strip bar (icon, message, end actions, dismiss) — with semantic intents, five palettes, and full light/dark support.
Playground
Installation
pnpm add @tessinaui/uiUsage
import { Banner } from "@tessinaui/ui";{/* Announcement strip — the thin page-top bar */}
<Banner
layout="strip"
body={<>Free shipping on orders over $50. <a href="/shipping">See details</a></>}
onDismiss={() => setHidden(true)}
/>
{/* System banner — semantic intent picks the plate, icon and role */}
<Banner
layout="strip"
intent="warning"
headline="Verify your email."
body="Confirm your address to keep sending messages."
actions={[{ label: "Resend email", variant: "outline" }]}
/>
{/* Promotional panel */}
<Banner
background="primary"
overline="Limited time"
headline="Summer styles, up to 40% off"
body="Refresh your wardrobe with thousands of deals."
actions={[{ label: "Shop the sale", href: "/sale" }]}
disclaimer="Offer ends Sunday. Selected items only."
/>
{/* Single-destination — the whole banner is one link */}
<Banner layout="strip" href="/collection" headline="Discover the new collection" />Compose the parts directly when you need full control:
import {
Banner, BannerOverline, BannerHeadline, BannerBody, BannerActions, BannerDisclaimer,
} from "@tessinaui/ui";
<Banner background="primary">
<BannerOverline>Limited time</BannerOverline>
<BannerHeadline>Summer styles, up to 40% off</BannerHeadline>
<BannerBody>Refresh your wardrobe with thousands of deals.</BannerBody>
<BannerActions>{/* your own buttons/links */}</BannerActions>
<BannerDisclaimer>Offer ends Sunday.</BannerDisclaimer>
</Banner>Banner or Alert?
The house follows the Atlassian Banner / SectionMessage split:
| Banner | Alert | |
|---|---|---|
| Scope | Whole page or a major section — announcements, promos, account-level system messages | One piece of content — a form, a card, a field group |
| Placement | Edge-to-edge above the content it governs (typically before the page h1), or a rounded block between sections | In the flow, next to the thing it describes |
| Shape | strip bar or promo panel | Message box with title/description/actions |
Form-validation summaries belong to the form (see FormErrorSummary), never to Banner.
Curving your own content
Content you put inside — an inset image or video poster — should curve like the chrome around it, or it reads as pasted on. The banner root publishes its inner radius as a CSS variable:
<img className="rounded-[var(--banner-item-radius)]" />It tracks whatever the component is actually doing: the explicit step when rounded is set (8px at md), and 0 at rounded="none" — so your content squares off exactly when the container does.
Examples
Default
Overline, headline, body, a call-to-action, and a disclaimer on the primary palette.
Strip
The thin announcement bar: inline link in the message, centered variant, an info mode-indicator with an end action, and a dismissible in-content strip.
Intents
info / success / warning / error in both weights. Icons come free per intent (override with icon, hide with icon={null}); severity always also lives in the text — never colour alone. Intents work on panels too.
Palettes
The five background fills — neutral, primary, inverse, accent, vivid — on panels and strips. inverse and vivid are locked plates: identical in light and dark.
Media
A full-bleed photo (with a legibility scrim), an inset side column, and an arbitrary decorative node with the overlay switched off.
Sizes
Panels scale padding, headline and body together. Strips take the same size prop but cap at md — the two strip densities differ only in row height, so they are not shown separately here.
Strip actions
An end button, a ghost text-action pair with a loading state, and a chevron drill-in over a whole-strip link.
Whole-banner link
href turns the entire banner into a single link, named by the headline. Explicit actions stay independently clickable above it.
Multi-destination
Pass several actions to point at multiple destinations.
Dismiss with persistence
The Smart-App-Banner rule: a dismissed promo stays dismissed on return visits. Persistence is consumer state — three lines with localStorage here.
Countdown & promo code
Timer chips and applied-code chips are badge-slot compositions — the banner ships no timer API.
Threshold progress
“$X away from free shipping” — compose the house Progress in compound children; the amount lives in the text, the bar reinforces it.
App banner
Smart-app-banner anatomy from existing slots: a logo in icon, a rating line in body, Open + fallback in actions.
Stacked banners
Prefer ONE banner at a time (combine messages). When stacking is unavoidable, order by severity — error above warning.
Video background
Motion media must ship a pause control (WCAG 2.2.2) and must not autoplay under reduced motion. The video is decorative; the message lives in text.
Consent bar
The Accept/Reject pair is the dismissal — no ✕. Pin it with fixed inset-x-0 bottom-0 z-50 in production and pad the page bottom so content never hides behind it.
Success with focus
The GOV.UK model for post-action success: pick ONE mechanism. This example moves focus, so the banner is a labelled region rather than a live region — announcing it as well would read the same sentence twice. Whatever takes focus must hand it back on dismiss. Never script focus to banners that exist on page load.
Anatomy
<Banner layout="panel">
{/* icon? — leading icon (auto per intent) */}
{/* badge? — logo / chip slot */}
<BannerOverline /> {/* eyebrow (panel only) */}
<BannerHeadline /> {/* heading (panel) / inline strong (strip) */}
<BannerBody /> {/* supporting copy */}
<BannerActions /> {/* CTAs — end slot on strips */}
<BannerDisclaimer /> {/* fine print (panel only) */}
{/* dismiss ✕ — when onDismiss is set */}
</Banner>Props
Banner
| Prop | Type | Default | Description |
|---|---|---|---|
layout | panel | strip | panel | Promotional hero vs thin announcement bar |
intent | none | info | success | warning | error | none | Semantic banner: paints the plate, auto-picks icon + live-region role. Wins over background |
variant | soft | solid | soft | Intent plate weight (tint vs on-color fill). Ignored while intent="none" |
background | neutral | primary | inverse | accent | vivid | neutral | Which fill to paint (Surface vocabulary). vivid/inverse are locked light-dark-identical plates |
media | image | inset | — | Panel media: full-bleed photo or side column |
size | sm | md | lg | md | Panel scale; strips cap at md |
rounded | none | sm | md | lg | full | panel lg · strip none | §2 cascade; full caps at the 24px panel radius |
align | start | center | start | Content alignment |
icon | ReactNode | null | auto per intent | Leading icon; null hides; any node (emoji, logo, thumb) replaces |
overline | ReactNode | — | Eyebrow label (panel only) |
headline | ReactNode | — | Title — heading in panels, inline strong in strips |
headingLevel | h1–h6 | h3 | Panel heading tag |
body | ReactNode | — | Supporting copy |
disclaimer | ReactNode | — | Fine print (panel only) |
actions | BannerAction[] | ReactNode | — | CTAs; first is prominent. Strips place them at the end |
badge | ReactNode | — | Chip/logo slot |
image / imageAlt | string | ReactNode / string | — | Media source for media |
overlay | auto | scrim | none | auto | Legibility treatment over a photo |
href | string | — | Whole-banner single link |
onDismiss / dismissLabel | () => void / string | — / Dismiss <headline> | Dismiss control; label auto-enriches with a string headline |
role | string | by intent | Overrides the live-region policy |
BannerAction: { label, href?, onClick?, variant?, intent?, size?, rounded?, loading?, leadingIcon?, trailingIcon? }.
Deprecated aliases (one minor, dev-warn once)
| Old | New | Notes |
|---|---|---|
tone="neutral | primary | inverse | accent | vivid" | background | Same values — §1 reserves tone for the on-color axis |
background="color | image | inset" | media (+ palette on background) | color → no media |
rounded="xl" | lg | The old 24px xl is the new full (§2 panel cap); full shrank 32→24px |
BannerSkeleton
Mirrors layout / size / rounded / media / align plus count-exact structure: lines, actions, dismissible, showIcon, showOverline, showDisclaimer. The plate and every bar derive from the component's own maps (§2b) — the CTA placeholder IS the button box.
Accessibility
- Live-region policy (override with
role):error/warning→role="alert"(announced immediately);info/success→role="status"(polite); promotional banners → a labelledregionwhen a propheadlinenames them, otherwise no role. Static page-load promos must never be alerts. - A dynamically inserted alert/status banner announces on mount; content that exists at first paint may not (mount it in response to the event, per Astryx guidance).
- Icons are decorative (
aria-hidden) — severity always also lives in the text. - The dismiss label auto-enriches with a string headline ("Dismiss Summer sale"), so stacked banners stay distinguishable to AT.
- With
hrefthe whole banner is one link named by the headline; explicit actions sit above it and stay independently clickable. - Strips render the headline as an inline
<strong>-weight span — a bar must not mint a document heading. Panels render a real heading; setheadingLevelto keep the outline valid. - Actions render house
Buttons: on-color plates hand them the published surface ink automatically. - Keyboard: everything interactive is a real button/link in natural tab order; the banner container itself is deliberately not focusable (no tab noise). Focus-on-mount for success banners is a consumer effect — see the example.
Content guidance
- One banner at a time; combine messages before stacking. If you must stack, order by severity.
warning/errorbanners should carry an action or link that resolves the problem.- Action labels are verb+noun ("Verify identity"), links destination-descriptive — never "Click here".
- Body stays at 1–2 sentences; put detail behind the action.
- Promos respect restraint: eligibility-gate and frequency-cap repeated banners, and persist dismissal (see the persistence example).
Agent notes
Stable hooks for tooling and AI agents: root data-slot="banner" with data-layout, data-intent/data-variant (semantic banners) or data-palette (promos); parts carry data-slot="banner-icon | banner-badge | banner-content | banner-overline | banner-headline | banner-body | banner-actions | banner-disclaimer | banner-media | banner-link | banner-dismiss". Async CTAs take loading per action. Content is free-form ReactNode everywhere; long copy wraps (strips reflow actions to a second row rather than truncating).