rapidUI

Banner

Page-wide message at the top of a page, such as planned maintenance or an outage, that can be dismissed.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/banner.json

Installs to @/components/ui/banner.

Manual installation

Install the dependencies:

pnpm add @base-ui/react@^1.8.0 class-variance-authority@^0.7.1 lucide-react@^1.48.0

Add the components it builds on: button.

Copy the source into your project:

components/ui/banner.tsx
'use client'

import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'
import { cva, type VariantProps } from 'class-variance-authority'
import { CircleAlertIcon, CircleCheckIcon, InfoIcon, TriangleAlertIcon, XIcon } from 'lucide-react'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'

// Each variant has its own tint and icon colour. The variant is also carried by the icon and the screen reader prefix,
// never by colour alone.
const bannerVariants = cva(
	'group/banner w-full border-b text-sm text-foreground [--banner-max-width:var(--container-7xl)]',
	{
		variants: {
			variant: {
				info: 'border-info-border bg-info-subtle [&_[data-slot=banner-icon]]:text-info-subtle-foreground',
				warning: 'border-warning-border bg-warning-subtle [&_[data-slot=banner-icon]]:text-warning-subtle-foreground',
				destructive:
					'border-destructive-border bg-destructive-subtle [&_[data-slot=banner-icon]]:text-destructive-subtle-foreground',
				success: 'border-success-border bg-success-subtle [&_[data-slot=banner-icon]]:text-success-subtle-foreground',
			},
		},
		defaultVariants: {
			variant: 'info',
		},
	},
)

type BannerVariant = NonNullable<VariantProps<typeof bannerVariants>['variant']>

const defaultIcons: Record<BannerVariant, React.ElementType> = {
	info: InfoIcon,
	warning: TriangleAlertIcon,
	destructive: CircleAlertIcon,
	success: CircleCheckIcon,
}

const defaultLabels: Record<BannerVariant, string> = {
	info: 'Information',
	warning: 'Warning',
	destructive: 'Error',
	success: 'Success',
}

function Banner({
	className,
	variant = 'info',
	role = 'status',
	icon,
	label,
	open: openProp,
	defaultOpen = true,
	onOpenChange,
	onDismiss,
	dismissible,
	closeLabel = 'Dismiss',
	children,
	...props
}: Omit<React.ComponentProps<'div'>, 'role'> &
	VariantProps<typeof bannerVariants> & {
		/**
		 * `status` (default) is announced politely when the message appears after the page has loaded. Use `alert` only for
		 * urgent messages, such as an ongoing outage; screen readers interrupt the user to read it.
		 */
		role?: 'status' | 'alert'
		/** Replace the variant's icon, or pass `false` to hide it */
		icon?: React.ReactNode
		/**
		 * Screen reader prefix that names the kind of message, such as "Warning". Defaults to an English label for the
		 * variant; pass a translation, or `false` when the title already says it.
		 */
		label?: string | false
		/** Controlled visibility */
		open?: boolean
		/** Initial visibility when uncontrolled */
		defaultOpen?: boolean
		/** Called with the new visibility when the banner is dismissed */
		onOpenChange?: (open: boolean) => void
		/** Called when the user dismisses the banner. Passing it shows the dismiss button */
		onDismiss?: () => void
		/** Show the dismiss button. Defaults to `true` when `onDismiss` or `onOpenChange` is passed */
		dismissible?: boolean
		/** Accessible name of the dismiss button */
		closeLabel?: string
	}) {
	const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
	const open = openProp ?? uncontrolledOpen
	const showDismiss = dismissible ?? (onDismiss !== undefined || onOpenChange !== undefined)
	const resolvedVariant: BannerVariant = variant ?? 'info'

	if (!open) return null

	const Icon = defaultIcons[resolvedVariant]
	const prefix = label === false ? null : (label ?? defaultLabels[resolvedVariant])

	function dismiss() {
		if (openProp === undefined) setUncontrolledOpen(false)
		onOpenChange?.(false)
		onDismiss?.()
	}

	return (
		<div
			data-slot="banner"
			data-variant={resolvedVariant}
			role={role}
			className={cn(bannerVariants({ variant: resolvedVariant }), className)}
			{...props}
		>
			<div
				data-slot="banner-container"
				className="mx-auto flex w-full max-w-(--banner-max-width) items-start gap-3 px-4 py-2.5 sm:px-6"
			>
				{icon === false ? null : (
					<span
						data-slot="banner-icon"
						aria-hidden="true"
						className="flex h-5 shrink-0 items-center [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4"
					>
						{icon ?? <Icon />}
					</span>
				)}
				<div data-slot="banner-content" className="flex min-w-0 flex-1 flex-wrap items-baseline gap-x-3 gap-y-1 leading-5">
					{prefix ? <span className="sr-only">{`${prefix}: `}</span> : null}
					{children}
				</div>
				{showDismiss ? (
					<Button
						data-slot="banner-close"
						variant="ghost"
						size="icon-sm"
						aria-label={closeLabel}
						onClick={dismiss}
						className="-my-1.5 -me-2 hover:bg-foreground/10 dark:hover:bg-foreground/10"
					>
						<XIcon aria-hidden="true" />
					</Button>
				) : null}
			</div>
		</div>
	)
}

function BannerTitle({ className, ...props }: React.ComponentProps<'p'>) {
	return <p data-slot="banner-title" className={cn('font-medium', className)} {...props} />
}

function BannerDescription({ className, ...props }: React.ComponentProps<'p'>) {
	return (
		<p
			data-slot="banner-description"
			className={cn('min-w-0 text-pretty wrap-break-word [&_a]:underline [&_a]:underline-offset-4', className)}
			{...props}
		/>
	)
}

// Renders a link by default. Use `render={<button type="button" />}` for an action that is not navigation.
function BannerAction({ className, render, ...props }: useRender.ComponentProps<'a'>) {
	return useRender({
		defaultTagName: 'a',
		props: mergeProps<'a'>(
			{
				className: cn(
					'inline-flex min-h-6 cursor-pointer items-center gap-1 rounded-sm font-medium whitespace-nowrap underline underline-offset-4 outline-none hover:decoration-2 focus-visible:ring-3 focus-visible:ring-ring [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
					className,
				),
			},
			props,
		),
		render,
		state: {
			slot: 'banner-action',
		},
	})
}

export { Banner, BannerTitle, BannerDescription, BannerAction, bannerVariants }

A page-wide message at the top of the page, such as planned maintenance, longer handling times or an outage. For a message about one part of the page, such as a form, use Alert instead.

Features

  • Four variants: info (default), warning, destructive and success, each with a tinted background and its own icon
  • The text keeps the normal text colour, so it stays readable in every variant, theme and colour mode
  • The variant is also given by the icon and a screen reader prefix ("Warning: "), not by colour alone
  • Spans the full width, with the content centred in a max-width container. It wraps on small screens
  • Composable BannerTitle, BannerDescription and BannerAction (a link, or a button with render)
  • Optional dismiss button, controlled (open and onOpenChange) or uncontrolled (defaultOpen)
  • role="status" by default, role="alert" for urgent messages. It never moves focus
  • All user-facing text can be overridden: label for the screen reader prefix and closeLabel for the dismiss button

Usage

import { Banner, BannerAction, BannerDescription, BannerTitle } from '@/components/ui/banner'
import { WrenchIcon } from 'lucide-react'

export function MaintenanceBanner() {
  return (
    <Banner icon={<WrenchIcon />} onDismiss={() => {}}>
      <BannerTitle>Planned maintenance on Saturday</BannerTitle>
      <BannerDescription>
        E-services are unavailable on Saturday 16 May 2026 from 22:00 to 02:00.
      </BannerDescription>
      <BannerAction href="/maintenance">Read more</BannerAction>
    </Banner>
  )
}

Place the banner first on the page, above the site header, so it is the first thing both sighted and screen reader users meet.

Examples

Default

Planned maintenance with a link to more information. icon replaces the variant's icon.

Variants

info for news and planned events, warning for things that may affect the user, destructive for outages and errors, and success when a problem is solved. The outage uses role="alert".

Dismissible

Pass onDismiss or onOpenChange to show the dismiss button. Control it with open to show it again, or to remember the choice. The banner leaves the page when it is dismissed, so move focus to a sensible place in onDismiss.

With Action

BannerAction renders a link by default. Use render={<button type="button" />} for an action that isn't navigation. closeLabel sets the name of the dismiss button.

Page Mockup

An urgent outage above the site header.

Props

PropTypeDefaultDescription
variant'info' | 'warning' | 'destructive' | 'success''info'The kind of message
role'status' | 'alert''status'alert only for urgent messages
iconReactNode | falsevariant's iconReplace the icon, or false to hide it
labelstring | falsevariant's labelScreen reader prefix: "Information", "Warning", "Error" or "Success"
openbooleanControlled visibility
defaultOpenbooleantrueInitial visibility when uncontrolled
onOpenChange(open: boolean) => voidCalled when the banner is dismissed
onDismiss() => voidCalled when the banner is dismissed
dismissiblebooleansee descriptionShow the dismiss button. true when onDismiss or onOpenChange is set
closeLabelstring'Dismiss'Accessible name of the dismiss button

Set --banner-max-width with className to match the width of your page layout, for example [--banner-max-width:var(--container-5xl)]. The default is --container-7xl (80rem).

Subcomponents

  • Banner: the full-width container with the icon, the content and the dismiss button
  • BannerTitle: the short headline, in medium weight
  • BannerDescription: the message. Links in it are underlined
  • BannerAction: a link (default) or a button with render, such as "Read more"

Accessibility

  • role="status" is polite: screen readers read the message when it appears after the page has loaded, without interrupting. Use role="alert" only for urgent messages, such as an outage that stops the user's task
  • A banner that is part of the page from the start is not announced as a live region, so place it first in the page where it is read before the header
  • The banner never moves focus when it appears
  • The variant is conveyed by the icon and a visually hidden prefix such as "Warning: ", not by colour alone. Translate the prefix with label, or pass label={false} when the title already says what kind of message it is
  • The text uses the normal text colour on a light tint, so it meets the 4.5:1 contrast minimum
  • The dismiss button is 32×32px and named by closeLabel. When the banner is dismissed it leaves the page, so move focus to a sensible place in onDismiss, for example the main heading
  • Links in the description and BannerAction are underlined and have a visible focus ring
  • Alert: messages about one part of the page, such as a form
  • Button