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.jsonInstalls 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.0Add the components it builds on: button.
Copy the source into your project:
'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,destructiveandsuccess, 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,BannerDescriptionandBannerAction(a link, or a button withrender) - Optional dismiss button, controlled (
openandonOpenChange) or uncontrolled (defaultOpen) role="status"by default,role="alert"for urgent messages. It never moves focus- All user-facing text can be overridden:
labelfor the screen reader prefix andcloseLabelfor 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
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'info' | 'warning' | 'destructive' | 'success' | 'info' | The kind of message |
role | 'status' | 'alert' | 'status' | alert only for urgent messages |
icon | ReactNode | false | variant's icon | Replace the icon, or false to hide it |
label | string | false | variant's label | Screen reader prefix: "Information", "Warning", "Error" or "Success" |
open | boolean | Controlled visibility | |
defaultOpen | boolean | true | Initial visibility when uncontrolled |
onOpenChange | (open: boolean) => void | Called when the banner is dismissed | |
onDismiss | () => void | Called when the banner is dismissed | |
dismissible | boolean | see description | Show the dismiss button. true when onDismiss or onOpenChange is set |
closeLabel | string | '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 buttonBannerTitle: the short headline, in medium weightBannerDescription: the message. Links in it are underlinedBannerAction: a link (default) or a button withrender, such as "Read more"
Accessibility
role="status"is polite: screen readers read the message when it appears after the page has loaded, without interrupting. Userole="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 passlabel={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 inonDismiss, for example the main heading - Links in the description and
BannerActionare underlined and have a visible focus ring
Related Components
- Alert: messages about one part of the page, such as a form
- Button