Banner React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React announcement banner: a full-width strip for the top of an app, page or panel, with tones, one action and a dismissal it remembers.
The strip says one thing to everyone: an icon, a bold title and the rest of the sentence on the same line, with one action at the end. Press the action and it shows a spinner while its promise runs; with only a link it is an underlined link with an arrow. The close button (Tab to it, then Enter or Space) collapses the strip, the page below slides up, and focus moves to the next control instead of getting lost; with persistKey the dismissal is remembered in local storage and the banner stays away, forever or for dismissDays days. Sticky banners stay pinned at the top of their scroller and fixed ones at the top of the viewport, below the notch, writing their height to --banner-offset so a topbar can move down. Banners that appear after the page loaded slide down from the top edge; with reduced motion they only fade. On a phone (below 640px) the banner turns compact: the description hides, the text keeps to two lines, the action becomes a chevron and the whole 44px row is the tap target, while the close button keeps its own 44px hit area. With announce on, screen readers hear the title politely when it appears, which suits offline notices.
Banner is a Pro React component in the React feedback components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add banner, together with Button, useControllableState, usePersistedState, Motion presets, Tones and 1 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add bannerBanner is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import Banner from @/components/ui/banner.
import { Banner } from "@/components/ui/banner"
export function ReleaseBanner() {
return (
<Banner
title="Version 3.2 is here"
description="New invoicing reports and Excel export."
action={{ label: "See what's new", href: "/whats-new" }}
persistKey="banner-v32"
/>
)
}Props
titledefault""ReactNodeThe bold lead of the sentence. The playground shows a sample when it is empty.
descriptiondefault""ReactNodeThe rest of the sentence, on the same line after a middot. Hidden in compact mode, so keep the title self-sufficient.
actionLabeldefault""stringShortcut for action.label: a small button at the end. The playground wires it to a fake 1.2s request.
action{ label: ReactNode; onClick?: () => void | Promise<unknown>; href?: string; target?: string }The one action. A promise from onClick shows the loading state until it settles. With only href it renders as an underlined link with an arrow. In compact mode the whole row presses it.
childrenReactNodeFree content instead of (or after) the title and description: a countdown, a progress bar.
iconReactNode | falseThe leading icon. Defaults per tone: neutral Megaphone, info Info, success CircleCheck, warning TriangleAlert, danger CircleAlert, brand Sparkles. false hides it.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Semantic color. Neutral solid is a black strip in light mode and a white one in dark mode.
colordefault""stringAny CSS color. Replaces the tone; text on a solid strip turns black or white by lightness.
variantdefault"solid""solid" | "soft" | "outline"solid for app-wide news, soft for a tinted strip (warnings, trials), outline for a hairline strip inside cards and panels.
sizedefault"md""sm" | "md"sm is at least 36px tall with 13px text, md 44px with 14px text.
defaultOpendefaulttruebooleanStarts visible. Use open and onOpenChange to control it; open wins over persistKey and nothing is stored then.
openbooleanControlled visibility. onOpenChange(false) fires on dismiss.
onOpenChange(open: boolean) => voidFires when the close button is pressed (also with persistKey).
onDismiss() => voidFires after the collapse animation finished.
dismissibledefaulttruebooleanShows the close button, with a 44px hit area on touch screens.
persistKeydefault""stringRemembers the dismissal ({ at: ISO date }) in local storage under this key. Nothing renders until storage is read, so a dismissed banner never flashes.
dismissDaysdefault0numberWith persistKey: 0 keeps the banner dismissed forever, otherwise it returns after this many days.
positiondefault"static""static" | "sticky" | "fixed"sticky pins it to the top of the nearest scroller, below var(--topbar-offset). fixed pins it to the viewport top with the safe area and writes its height to --banner-offset on <html> (0 while hidden). One fixed banner at a time; stack several as static banners in one fixed wrapper.
aligndefault"center""center" | "start"Where the content row sits. Centered text stays centered even with a close button at the end.
contentWidthdefault"page""full" | "page"page caps the content row at max-w-6xl like a page; full uses the whole width (panels, cards).
compactdefault"auto""auto" | "always" | "never"auto goes compact below 640px: the description hides, the text keeps to two lines, the action becomes a chevron and the whole row is its tap target.
hrefdefault""stringMakes the whole banner a link. The close button and the action stay separate.
announcedefaultfalsebooleanReads the title in a polite live region when the banner appears after mount. For offline style notices.
safeAreadefaulttruebooleanA fixed banner pads env(safe-area-inset-top), so it clears the notch.
labelsPartial<{ region: string; dismiss: string }>The region's aria-label ("Announcement") and the close button's aria-label ("Dismiss"). DEFAULT_BANNER_LABELS has the English set.
classNamesPartial<Record<"inner" | "body" | "icon" | "content" | "title" | "description" | "action" | "close", string>>Extra classes for the inner parts.
classNamestringExtra classes for the strip itself, merged last.
Parts
DEFAULT_BANNER_LABELS
constantThe English labels: region ("Announcement", the landmark's aria-label) and dismiss ("Dismiss", the close button). Type: BannerLabels.
BannerAction
typeThe action prop: { label, onClick?, href?, target? }. A returned promise keeps the action loading until it settles; with only href it is an underlined link with an arrow.
BannerDismissal
typeWhat persistKey stores in local storage: { at }, the ISO time of the dismissal, read against dismissDays.
Examples
Variants and tones
solid, soft and outline in the six tones. Neutral solid is black in light mode and white in dark.
Maintenance
A warning soft strip that cannot be dismissed, with a custom icon.
Trial ending
Brand belongs in examples. The action's promise shows a spinner, then the copy changes.
Offline
Controlled by the browser's online and offline events; announce reads it to screen readers.
Sale with an end date
A custom color, a live countdown in the description and a link action.
Inside a card
outline, sm and full width over a list. Sending the invoices collapses the strip.
Sticky and remembered
Sticks to the top of the scroller; persistKey keeps it closed after a reload.
Files
- components/ui/banner.tsx
FAQ
Is the React Banner component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Banner?
Run npx wingo-ui@latest add banner. It also adds the Wingo UI items it builds on (Button, useControllableState, usePersistedState, Motion presets, Tones and 1 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/banner.
How does Banner behave on phones?
On a phone (below 640px) the banner turns compact: the description hides, the text keeps to two lines, the action becomes a chevron and the whole 44px row is the tap target, while the close button keeps its own 44px hit area.