A page-wide announcement with semantic tones, optional action, and controlled or local dismissal.
import { Banner } from "@/components/ballmac/banner"
export default function BannerDemo() {
return (
<Banner
className="w-full max-w-lg"
title="A new workspace view is ready"
description="Your team can switch to the updated layout at any time."
action={
<a
href="#workspace"
className="text-primary rounded-sm text-sm font-semibold underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
Explore the update
</a>
}
dismissible
/>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/bannerInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/banner.tsx// Ballmac UI: Banner. https://ui.ballmac.com/components/banner "use client" import * as React from "react" import { CheckCircle2, Info, TriangleAlert, X } from "lucide-react" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type BannerProps = Omit<React.ComponentProps<"section">, "title"> & { /** Short headline that names the announcement. */ title: string /** Supporting detail, kept concise for a page-wide banner. */ description?: React.ReactNode /** Semantic tone of the announcement. */ tone?: "info" | "success" | "warning" /** Optional link or button placed after the message. */ action?: React.ReactNode /** Show a dismiss button. */ dismissible?: boolean /** Controlled visibility. */ visible?: boolean /** Initial visibility when uncontrolled. */ defaultVisible?: boolean /** Called when visibility changes. */ onVisibleChange?: (visible: boolean) => void } function Banner({ className, title, description, tone = "info", action, dismissible = false, visible, defaultVisible = true, onVisibleChange, ...props }: BannerProps) { const msg = useMessages() const [internalVisible, setInternalVisible] = React.useState(defaultVisible) const shown = visible ?? internalVisible if (!shown) return null const Icon = tone === "success" ? CheckCircle2 : tone === "warning" ? TriangleAlert : Info function dismiss() { if (visible === undefined) setInternalVisible(false) onVisibleChange?.(false) } return ( <section data-slot="banner" aria-label={title} className={cn( "grid w-full min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-x-3 gap-y-2 rounded-xl border px-4 py-3 text-sm shadow-sm sm:flex sm:flex-wrap sm:items-center", tone === "info" && "border-primary/25 bg-primary/5", tone === "success" && "border-chart-2/30 bg-chart-2/10", tone === "warning" && "border-chart-3/30 bg-chart-3/10", className, )} {...props} > <Icon aria-hidden="true" className="text-foreground col-start-1 row-start-1 mt-0.5 size-4 shrink-0 sm:mt-0" /> <div className="col-start-2 row-start-1 min-w-0 flex-1 leading-5"> <strong className="font-semibold">{title}</strong> {description && ( <span className="text-muted-foreground block sm:ms-1.5 sm:inline">{description}</span> )} </div> {action && ( <div data-slot="banner-action" className="col-start-2 row-start-2 shrink-0"> {action} </div> )} {dismissible && ( <button type="button" aria-label={msg("banner.dismiss", "Dismiss {title}", { title })} onClick={dismiss} className="text-muted-foreground hover:bg-accent hover:text-foreground col-start-3 row-start-1 flex size-8 shrink-0 items-center justify-center rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" > <X aria-hidden="true" className="size-4" /> </button> )} </section> ) } export { Banner, type BannerProps }Update the import paths to match your project setup.
Usage
import { Banner } from "@/components/ballmac/banner"The full example is in the Code tab above.
Examples
States and variants
import { Banner } from "@/components/ballmac/banner"
export default function BannerStates() {
return (
<div className="flex w-full max-w-lg flex-col gap-3">
<Banner
title="Everything is synced"
description="All changes are saved."
tone="success"
/>
<Banner
title="Review your connection"
description="Updates may take longer than usual."
tone="warning"
dismissible
/>
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
title*Short headline that names the announcement. | string | — |
descriptionSupporting detail, kept concise for a page-wide banner. | React.ReactNode | — |
toneSemantic tone of the announcement. | "info" | "success" | "warning" | "info" |
actionOptional link or button placed after the message. | React.ReactNode | — |
dismissibleShow a dismiss button. | boolean | false |
visibleControlled visibility. | boolean | — |
defaultVisibleInitial visibility when uncontrolled. | boolean | true |
onVisibleChangeCalled when visibility changes. | (visible: boolean) => void | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab / Enter | Focus and activate the action or dismiss control |
Use with AI
A page-wide announcement with semantic tones, optional action, and controlled or local dismissal. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Banner (@ballmac/banner) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Announce a page-level update
- Keep a persistent notice above page content
Not for
- Use alert for content scoped to one panel
Registry JSON: https://ui.ballmac.com/r/banner.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Alert
A semantic callout with five theme-aware tones, clear icon placement, an action row, and opt-in urgent announcements.
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Alert Dialog
Focus-managed confirmation for consequential actions, with a clear cancel path, optional media, and a token-based destructive action.
Callout
An editorial guidance panel with a clear kind label, icon, title, and optional action.