Skip to the content

ComponentsMessages

Banner

A message across the full width of the page, about the page or the whole product, with optional actions and a dismiss button.

Import

import { Banner, BannerTitle, BannerDescription, BannerActions } from "@booleanpress/ui/banner"

Usage

Place a banner at the top of the page, or of the area it is about, for news that stays true while the page is open: a paused service, an expiring licence, new limits. For a message about one form or one card, use Alert; for news that comes and goes, a toast.

import { Banner, BannerActions, BannerDescription, BannerTitle } from "@booleanpress/ui/banner"
import { Button } from "@booleanpress/ui/button"

export function LicenceBanner() {
  return (
    <Banner tone="warning" dismissible>
      <BannerTitle>Your licence expires in 5 days</BannerTitle>
      <BannerDescription>Renew it to keep receiving updates.</BannerDescription>
      <BannerActions>
        <Button size="sm">Renew licence</Button>
      </BannerActions>
    </Banner>
  )
}

tone sets the colours and the icon: neutral, info (the default), success, warning or destructive. icon replaces the icon, and icon={null} removes it. BannerTitle, BannerDescription and BannerActions sit on one line when there is room and wrap when there is not.

dismissible adds a × that removes the banner and calls onDismiss; remember the choice yourself if it should stay gone. position="sticky" keeps it at the top of the scrolling box while the content scrolls under it. In the WordPress admin, where the admin bar covers the top of the window, add className="top-(--wp-admin--admin-bar--height)" when the page itself scrolls.

Examples

Tones

neutral, info, success, warning and destructive, each with its own icon.

import { Banner, BannerDescription, BannerTitle } from "@booleanpress/ui/banner"

export default function BannerTones() {
  return (
    <div className="flex w-full flex-col gap-3">
      <Banner tone="neutral">
        <BannerDescription>Scheduled maintenance on 12 October 2026, 02:00–03:00 UTC.</BannerDescription>
      </Banner>
      <Banner tone="info">
        <BannerTitle>New sending limits</BannerTitle>
        <BannerDescription>Free plans can send 500 emails a day from 1 November 2026.</BannerDescription>
      </Banner>
      <Banner tone="success">
        <BannerDescription>Your domain example.com is verified. Emails are signed with DKIM.</BannerDescription>
      </Banner>
      <Banner tone="warning">
        <BannerDescription>The Primary mailer has used 90% of this month’s quota.</BannerDescription>
      </Banner>
      <Banner tone="destructive">
        <BannerTitle>Sending is paused</BannerTitle>
        <BannerDescription>The SMTP server refused the last 25 connections.</BannerDescription>
      </Banner>
    </div>
  )
}

With actions

BannerActions at the end of the text: Renew licence and Remind me later.

import { Banner, BannerActions, BannerDescription, BannerTitle } from "@booleanpress/ui/banner"
import { Button } from "@booleanpress/ui/button"

export default function BannerWithActions() {
  return (
    <Banner tone="warning" className="w-full">
      <BannerTitle>Your licence expires in 5 days</BannerTitle>
      <BannerDescription>Renew it to keep receiving updates and support.</BannerDescription>
      <BannerActions>
        <Button size="sm" variant="outline">
          Remind me later
        </Button>
        <Button size="sm">Renew licence</Button>
      </BannerActions>
    </Banner>
  )
}

Dismissible

dismissible adds a ×; when it removes the banner, returnFocusTo sends focus to the button that shows it again.

import { useEffect, useRef, useState } from "react"
import { Banner, BannerDescription } from "@booleanpress/ui/banner"
import { Button } from "@booleanpress/ui/button"

export default function BannerDismissible() {
  const [key, setKey] = useState(0)
  const [dismissed, setDismissed] = useState(false)
  const showAgain = useRef<HTMLButtonElement>(null)
  const wrapper = useRef<HTMLDivElement>(null)

  // The banner came back from "Show the banner again", which is gone: focus its ×.
  useEffect(() => {
    if (key > 0) wrapper.current?.querySelector<HTMLElement>("[data-slot=banner-dismiss]")?.focus()
  }, [key])

  return (
    <div ref={wrapper} className="flex w-full flex-col items-start gap-3">
      <Banner key={key} tone="info" dismissible onDismiss={() => setDismissed(true)} returnFocusTo={showAgain}>
        <BannerDescription>Version 2.4 adds per-site sending limits. Read what changed in the release notes.</BannerDescription>
      </Banner>
      {dismissed && (
        <Button
          ref={showAgain}
          variant="outline"
          size="sm"
          onClick={() => {
            setDismissed(false)
            setKey((value) => value + 1)
          }}
        >
          Show the banner again
        </Button>
      )}
    </div>
  )
}

Sticky

position="sticky" keeps the banner at the top of a scrolling log.

import { Banner, BannerActions, BannerDescription } from "@booleanpress/ui/banner"
import { Button } from "@booleanpress/ui/button"

const ENTRIES = Array.from({ length: 12 }, (_, index) => ({
  id: 4120 - index,
  to: `customer${index + 1}@example.com`,
}))

export default function BannerSticky() {
  return (
    <div className="h-64 w-full overflow-y-auto rounded-md border" role="region" tabIndex={0} aria-label="Email log">
      <Banner tone="destructive" position="sticky">
        <BannerDescription>12 emails bounced in the last hour.</BannerDescription>
        <BannerActions>
          <Button size="sm" variant="outline">
            Review bounces
          </Button>
        </BannerActions>
      </Banner>
      <ul className="divide-y text-sm">
        {ENTRIES.map((entry) => (
          <li key={entry.id} className="flex justify-between px-4 py-2.5">
            <span>{entry.to}</span>
            <span className="text-muted-foreground tabular-nums">#{entry.id}</span>
          </li>
        ))}
      </ul>
    </div>
  )
}

Accessibility

Semantics
A div with role="status" for the neutral, info and success tones and role="alert" for warning and destructive, so a banner added while the page is open is announced; pass role to change it. The icon is decorative.
Labels
Say the whole message in the text: the colour and the icon only repeat it. The × is named by the provider's dismiss string.
Focus
The banner itself takes no focus; its actions and × are in the page's tab order. When the × removes it with focus on it, focus moves to returnFocusTo, or else to the first element after the banner that Tab reaches (the last one before it when nothing follows). returnFocusTo is read once the banner has gone, so it can name an element the dismissal shows.
Known limits
  • A banner present when the page loads is not announced; screen-reader users find it in reading order. Put it first.
  • Show one banner at a time per area; several in a row compete for attention.

Keyboard

Keyboard
KeyBehaviour
EnterorSpaceOn the ×, removes the banner and moves focus on.
TabMoves through the banner's actions and its ×, in reading order.

API

Banner

Renders a div and passes it every other prop.

Banner props
PropTypeDefaultDescription
dismissiblebooleanfalseRender a × button that removes the banner.
iconReactNodeThe icon before the text. Each tone has its own; pass another, or null for none.
onDismiss(() => void)Called when the × removes the banner.
position"static" | "sticky" | nullstaticstatic (default) or sticky: sticks to the top of the scrolling box.
returnFocusToReturnFocusTargetWhere focus goes when the × removes the banner. By default, the first element after the banner that Tab reaches (the last one before it when nothing follows). Read once the banner has gone, so it can name an element that the dismissal itself shows, such as a "Show again" button.
tone"destructive" | "success" | "info" | "warning" | "neutral" | nullinfoneutral, info (default), success, warning or destructive: the colours, the default icon and the live role.

BannerTitle

Renders a p and passes it every other prop.

BannerDescription

Renders a p and passes it every other prop.

BannerActions

Renders a div and passes it every other prop.

Also exported: bannerVariants, the class names of Banner's variants and sizes (cva), to give another element the same look.

Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.

Data attributes: data-slot="banner" (Banner), data-slot="banner-title" (BannerTitle), data-slot="banner-description" (BannerDescription), data-slot="banner-actions" (BannerActions), and data-tone, data-position.

Provider strings: dismiss (BooleanUIProvider's strings).

Theming

Each tone is the Message colours: --{tone}-subtle fill, --{tone}-border edge and --{tone}-strong text; neutral uses --secondary, --secondary-foreground and --border.

The tokens its classes read. Change their values in your theme, and it follows.

Theme tokens
TokenUsed for
--borderborder
--destructive-borderborder
--destructive-strongtext
--destructive-subtlebackground
--info-borderborder
--info-strongtext
--info-subtlebackground
--secondarybackground
--secondary-foregroundtext
--success-borderborder
--success-strongtext
--success-subtlebackground
--warning-borderborder
--warning-strongtext
--warning-subtlebackground