Components · Feedback indicators

Banner

A tinted bar that delivers a status, a confirmation, or a blocking warning to the user without interrupting their flow. Five tones, two types, two positions, and an optional dismiss action.

Documentedby Derek Fidler

Order saved. View orders

Overview

A banner sits inside the page (or pinned to its top) and reports something the user needs to know — an order saved, a payment declined, a setting that needs verification. It carries a tone (what kind of news), a type (how much copy), and a position (where on the page). The body of the page keeps working; the banner just informs.

Banner is for page-scope news

Reach for a banner when the message applies to the whole surface (the form, the order, the settings page). For field-specific errors, use the inline messagebelow the input. For transient confirmations that don't block anything, a toast is friendlier — it appears, announces, and disappears on its own.

Anatomy

Five parts. The container takes the tone-tinted background and tone-tinted text. Inside it, a 24 px leading icon names the tone, a message body carries the copy, and an optional 48 × 48 dismiss button sits trailing.

Order saved. View orders

  1. Container

    Tinted background drawn from background.accent.<hue>.subtlest per tone. 56 px minimum height. 4 px rounded corners on embedded; flat on page-top.

  2. Tone icon

    24 px Material Outlined glyph in the tone's text colour. check (success), warning_amber (caution + warning), error_outline (critical), info (info). The icon doubles up the tone — never replaces it.

  3. Message

    Inter Tight Medium 16/20 in the tone's text colour. Single-line types pair the message with an optional inline action; multi-line types stack a heading + bullet list.

  4. Inline action (optional)

    Underlined link in the tone's text colour. Sits at the end of the message — “View orders”, “Resolve”, “Learn more”. Single action only; for two or more, drop the link and use a separate button row.

  5. Dismiss (optional)

    48 × 48 ghost button with a 24 px close glyph. Drop entirely when the banner is blocking — the user must act on it.

Tones

Five tones, each tied to a specific kind of news. Pick the tone the page is announcing — never the tone you wish the page were announcing. A successful action that feels precarious is still success.

Order saved. View orders

Order needs review. View orders

Select a table to move. View orders

Caution and warning use the same icon

Both reach for warning_amber. The difference is colour — caution uses yellow (a soft hint), warning uses orange (something needs attention). Never use red for either; red is reserved for critical (the action stopped, something failed).

Types

Two layouts cover almost every banner in the product. Single-line for status updates and confirmations; multi-line for validation summaries that list multiple problems.

Single line

One line of copy, an optional inline action. Use when the message fits in a sentence — confirmations, simple status changes, dismissible tips.

Multi line · bullets

Heading + bullet list. Use when the user needs to act on multiple items inside the same surface — validation summaries, compliance warnings, change logs.

Position

Two positions. Page-top spans the viewport and sits above the page content — use it when the message is about the whole page or session. Embedded sits inside a section, scoped to its surrounding context.

Select a table to move. View tables

Page content sits below the page-top banner. The banner spans the full page width with no rounded corners.

Embedded

Sits inside a section of the page. 4 px rounded corners. Width matches the surrounding column.

Order needs review. Review

Dismiss

The dismiss action lets the user clear the banner manually. Default-on for confirmations and non-blocking notices; default-off for blocking ones — the user must resolve the underlying issue, not dismiss the message.

Dismissable

Order saved. View orders

The default. Trailing 48 × 48 ghost button with a 24 px close glyph. Use for transient confirmations and non-blocking warnings.

Non-dismissable

Drop the dismiss when the message is blocking — the user must act on it before the page can do its job. Critical compliance, missing required data, paused-account warnings.

Behavior

  • Banners persist until dismissed or resolved. Unlike a toast, they stay visible. The user closes them explicitly (dismissable) or the underlying state changes (validation passes, settings update). Don't auto-fade.
  • One banner per scope. At most one banner per page-top, one per embedded section. Stacking banners reads as noise — the user stops reading after the second. If the page genuinely has multiple problems, fold them into a single multi-line banner.
  • The inline action is one verb. “View orders”, “Resolve”, “Learn more”. If you need two paths, the banner isn't the right surface — reach for a dialog or a sheet.
  • Dismissed banners don't come back on the same session. When the user dismisses a banner, persist that dismissal for the session (or the user, when authenticated). Re-rendering the same banner on a route change feels broken.
  • Banners enter and exit, they don't pop. 200 ms fade + a small translateY on entry. Honour prefers-reduced-motion — keep the fade, drop the translate.

Banner vs. inline message vs. toast

Three feedback surfaces share the same tone palette but earn different jobs.

  • Banner

    Page-scope. Persistent. Tinted bar with a leading tone icon and an optional dismiss. Use for status that applies to the whole surface — order saved, account paused, validation summary at the top of a form.

  • Inline message

    Field-scope. Persistent. Single-line helper or error directly below the input. Use for feedback specific to one form field — “Phone must include country code”.

  • Toast

    Session-scope. Transient. Floats over the page, auto-fades after a few seconds. Use for confirmations the user can acknowledge without acting — “Saved.”, “Copied to clipboard.”

Accessibility

  • Role per tone. role="alert" for critical and warning — interrupts current screen-reader output. role="status" for success, caution, and info — announces politely without interrupting. Don't apply role="alert" to a confirmation; it interrupts and feels rude.
  • Tone never carries meaning alone. Every banner pairs the colour with an icon and a message. Screen-reader users and people with colour-vision differences both read the message from the icon + copy.
  • Inline action is a button or a link. When it navigates, use <a>; when it performs an action, <button>. Don't style a <span> with onClick to look like a link.
  • Dismiss button is labelled.A bare close glyph isn't enough — aria-label="Dismiss" (or a more specific verb when the action does more than dismiss the banner).
  • Focus management: page-top banners with role="alert" don't steal focus. Embedded banners that announce a validation failure can move focus to the first invalid field, not to the banner itself.
  • Contrast: every text colour ships at WCAG AA against its tone background. The 900 step of each hue is the minimum; never lighten for aesthetic reasons.

Code

One Banner component handles every variant. The tone is the only required prop — type, position, and dismiss have sensible defaults.

tsx

import { Banner } from "@flatpay-dk/ui";

// Default — single-line, embedded, dismissable
<Banner tone="success" action={<a href="/orders">View orders</a>}>
  Order saved.
</Banner>

// Critical — multi-line bullets, non-dismissable
<Banner
  tone="critical"
  type="multiLine"
  position="embedded"
  title="Errors found"
  bullets={[
    "Settlement account is missing a routing number.",
    "Beneficial owner address is incomplete.",
  ]}
  dismissable={false}
/>

// Page-top info banner with a single inline action
<Banner
  tone="info"
  position="pageTop"
  action={<a href="/tables">View tables</a>}
>
  Select a table to move.
</Banner>

// Caution with no action — the message itself is the news
<Banner tone="caution" dismissable>
  Order needs review before it can ship.
</Banner>

// Persisting dismissal
const [dismissed, setDismissed] = useLocalStorage("banner.kyc", false);

{!dismissed && (
  <Banner
    tone="warning"
    onDismiss={() => setDismissed(true)}
    action={<a href="/kyc">Verify</a>}
  >
    Settlement account needs verification.
  </Banner>
)}

Best practices

Banners are read fast. The defaults below keep them legible without shouting.

Order saved. View orders

Do

Lead with the news. 'Order saved' as the first words; the action follows. The user knows what happened in one glance.

We're delighted to inform you that your order has been saved successfully.

Don't

Don't bury the news in a sentence. 'We're delighted to inform you that…' is marketing voice; banners are operator voice.

Do

Drop the dismiss for blocking warnings. The user has to resolve the underlying issue — closing the banner doesn't help.

Don't

Don't make a critical message dismissable. The user closes it, the issue stays — and the next page load makes it look like nothing was wrong.

Do

Fold multiple problems into one multi-line banner. The user reads one banner and gets a punch list.

Don't

Don't stack three single-line banners. Two is already noisy; three is a wall the user stops reading.

Props

PropTypeDefaultDescription
tone"success" | "caution" | "warning" | "critical" | "info""info"What kind of news the banner is announcing. Drives background, text, icon, and ARIA role.
type"singleLine" | "multiLine""singleLine"Single-line for one sentence + optional inline action. Multi-line for a heading + bullet list of details.
position"pageTop" | "embedded""embedded"Page-top spans the full viewport, no rounded corners. Embedded sits inside a section with 4 px corners.
titlestringHeading rendered above the bullet list when type="multiLine". Inter Tight Medium 16/20.
bulletsstring[]Bullet list rendered when type="multiLine". Each item carries the tone's text colour.
childrenReactNodeSingle-line message body. Used when type="singleLine".
actionReactNodeInline action — typically an anchor or button rendered as an underlined link at the end of the message.
dismissablebooleantrueRenders the trailing 48 × 48 close button. Drop for blocking messages the user must resolve.
onDismiss() => voidFires when the dismiss button is clicked. Persist the dismissal so the banner doesn't return on the next render.