Banner
A banner states a fact about the current state of a surface — not something that just happened, and not commentary inside prose. It is the only component allowed to span the full width of a page, which is why there is never more than one of them in view.
Which component is this?
Four components carry messages and they are not interchangeable. Choosing wrongly is the most common defect in review, so the test is written as a single question each.
| Component | Answers | Lifetime | Placement |
|---|---|---|---|
| Banner | What is true about this surface right now? | As long as the fact is true | Top of the scope, full width |
| Callout | What should I know about this passage? | Permanent — it is content | Inline, in the flow |
| Toast | What just happened? | Seconds | Floating, above everything |
| Dialog | What must I decide before continuing? | Until answered | Centred, blocking |
The giveaway for a misused banner is the past tense. “Your review was published” is a toast. “This event has not been verified” is a banner — it is still true on the next page load, and on the one after that.
Scopes
Two scopes exist and the difference is what the fact is about. A global banner is about the product; a scoped banner is about the one object on screen. They never appear together — if the product is degraded, nothing about a single event is worth saying.
Global
Above the header, edge to edge, about the whole product. Reserved for incidents, scheduled maintenance and policy changes that alter what the product does.
Scoped
Inside a surface, at the top of the object it describes. This is the one that carries product meaning for us: an unverified event, a closed rating window, an organiser's own page awaiting a claim.
Tones
There is no success banner. Success is momentary by definition, and a persistent green bar telling someone their review published is still there tomorrow, still claiming it just happened. Success belongs to Toast.
Specification
| Property | Value | Why |
|---|---|---|
| Tones | neutral · warning · destructive | No success, no primary — a banner is not a promotion |
| Fill | The tone's -subtle token | Solid tone fills at full width are the loudest thing a page can do |
| Boundary | 1px hairline, bottom only when global | The system is flat; a banner separates with a line, never a shadow |
| Icon | 20px Lucide, tone-coloured, aria-hidden | Redundant with the text — colour is never the only signal |
| Radius | 0 when global, --radius-md when scoped | A global banner is edge to edge; there is no corner to round |
| Max per scope | 1 | Two banners means neither is read |
| Role | role="region" + aria-label | Landmark, not an alert — it did not just happen |
| Dismiss | Only when non-blocking; 44px target | A fact the user cannot act on must not be dismissible |
Do & don't
Structure
<!-- Landmark, not an alert. A banner is state, not an event. -->
<div class="banner banner--neutral" role="region" aria-label="Event status">
<span class="banner__icon" aria-hidden="true"><!-- lucide:info --></span>
<div class="banner__body">
<strong>This event has not been verified.</strong>
It will not appear in regional rankings until an organiser claims it.
<a href="/events/123/claim">Claim this event</a>
</div>
<!-- Only when the fact is non-blocking. Omit the button entirely otherwise. -->
<button class="banner__dismiss" type="button" aria-label="Dismiss"></button>
</div>Use role="status" instead of role="region" in the one case where a banner appears in response to something the user did — a failed background sync, say. Never role="alert": it interrupts whatever a screen reader is saying, which is only justified for something that has just gone wrong and is about to cost the user work.
Rules
- One banner per scope. If two facts are true, the more severe one wins and the other waits. Stacking them trains people to skip the region.
- Never a marketing surface. Every banned word from the anti-slop list applies here, and the Deletion Test applies harder: a banner that can be deleted without changing what the user understands about the page was never a banner.
- Dismissal must persist — per user and per fact, not per session. A banner that returns on every navigation is worse than one that cannot be dismissed at all.
- Non-blocking facts only may be dismissed. “This event is unverified” stays, because dismissing it would let someone read an unverified rating as a verified one.
- No animation on entry. A banner is present when the page renders; sliding it in makes a static fact look like an event.
- Links inside a banner are underlined, as everywhere else. Tone colour is not enough to mark a link inside a tone-coloured region.
theme.css.