Banner
A status message that sits inline in the page and stays until it's dismissed or the condition it reports clears.
React Aria has no Banner primitive — a banner isn't an overlay, so it needs
none of the focus trapping, portalling, or queueing that Dialog and Toast
provide. What it does need is a live region and a real button: Banner.Root
renders an ARIA live region and Banner.Close is a React Aria Button, which
is where its hover, press, and focus-visible styling comes from.
Reach for Toast instead when the message is transient and not tied to a place on the page.
import { Banner } from "@pockets-ui/components"Anatomy
| Part | Role |
|---|---|
Banner.Root | The surface and live region. Owns intent, variant, size, and dismissal state. |
Banner.Header | The first line: Icon, Title, Close. |
Banner.Icon | Leading glyph. Defaults to the Lucide icon for the intent; decorative. |
Banner.Title | The headline. A <p> by default — pass as="h2" where it opens a section. |
Banner.Close | Dismiss button (React Aria Button). |
Banner.Content | The body below the header — description, or whatever else you stack. |
Banner.Description | Supporting copy. |
Banner.Actions | Buttons or links, below the body. |
Root is a column: the header row sits over Content and Actions, which run
the full width of the banner rather than indenting under the title. A one-line
banner is just a Header — no Content needed.
<Banner.Root intent="warning">
<Banner.Header>
<Banner.Icon />
<Banner.Title>Heads up</Banner.Title>
<Banner.Close />
</Banner.Header>
<Banner.Content>
<Banner.Description>Your trial ends Friday.</Banner.Description>
</Banner.Content>
</Banner.Root>Basic
Code
import { Banner, Button } from "@pockets-ui/components"
export default function BannerBasic() {
return (
<Banner.Root intent="warning">
<Banner.Header>
<Banner.Icon />
<Banner.Title>Your trial ends Friday</Banner.Title>
<Banner.Close />
</Banner.Header>
<Banner.Content>
<Banner.Description>
Add a payment method to keep your team's projects active.
</Banner.Description>
</Banner.Content>
<Banner.Actions>
<Button size="sm" variant="outline">
Add payment
</Button>
</Banner.Actions>
</Banner.Root>
)
}Intents
intent picks the semantic color set and the default icon. All four carry
_dark values, so dark mode needs nothing extra.
Code
import { Banner, type BannerIntent } from "@pockets-ui/components"
const MESSAGES: Record<BannerIntent, string> = {
info: "Scheduled maintenance runs Sunday at 02:00 UTC.",
success: "Your changes were published.",
warning: "Two of your integrations need reauthorization.",
danger: "We couldn't process your last payment.",
}
export default function BannerIntents() {
return (
<div style={{ display: "grid", gap: "0.75rem", width: "100%" }}>
{(Object.keys(MESSAGES) as BannerIntent[]).map((intent) => (
<Banner.Root key={intent} intent={intent}>
<Banner.Header>
<Banner.Icon />
<Banner.Title>{MESSAGES[intent]}</Banner.Title>
</Banner.Header>
</Banner.Root>
))}
</div>
)
}Variants
variant is orthogonal to intent: subtle tints the surface, solid fills
it, outline draws only the border and inherits whatever page surface it sits
on.
Code
import { Banner } from "@pockets-ui/components"
export default function BannerVariants() {
return (
<div style={{ display: "grid", gap: "0.75rem", width: "100%" }}>
{(["subtle", "solid", "outline"] as const).map((variant) => (
<Banner.Root key={variant} intent="danger" variant={variant} size="sm">
<Banner.Header>
<Banner.Icon />
<Banner.Title>variant="{variant}"</Banner.Title>
</Banner.Header>
</Banner.Root>
))}
</div>
)
}Dismissal
Banner.Close hides the banner on its own — no state needed for the common
case. Pass isOpen to Root to take control; Banner.Close then only calls
onOpenChange(false) and leaves visibility to you.
Code
import { Banner, Button } from "@pockets-ui/components"
import { useState } from "react"
export default function BannerControlled() {
const [isOpen, setIsOpen] = useState(true)
return (
<div style={{ display: "grid", gap: "0.75rem", width: "100%" }}>
<Banner.Root
intent="info"
isOpen={isOpen}
onOpenChange={setIsOpen}
size="sm"
>
<Banner.Header>
<Banner.Icon />
<Banner.Title>
Dismissing this fires <code>onOpenChange</code>; the parent decides.
</Banner.Title>
<Banner.Close />
</Banner.Header>
</Banner.Root>
{!isOpen && (
<Button size="sm" variant="outline" onPress={() => setIsOpen(true)}>
Bring it back
</Button>
)}
</div>
)
}Announcement
Banner.Root is role="status" by default, which waits for a pause in speech
before announcing. Pass role="alert" to interrupt — reserve it for messages
the user has to act on right now, such as a failed payment blocking checkout.
<Banner.Root intent="danger" role="alert">
<Banner.Header>
<Banner.Icon />
<Banner.Title>Payment failed</Banner.Title>
</Banner.Header>
</Banner.Root>Theming
intent sets five custom properties that variant reads, so you can retheme a
single banner without forking the recipe:
<Banner.Root
css={{
"--pk-banner-bg": "token(colors.bg.premium)",
"--pk-banner-fg": "token(colors.fg.premium)",
"--pk-banner-border": "token(colors.border.premium)",
}}
>
…
</Banner.Root>The solid variant reads --pk-banner-solid-bg / --pk-banner-solid-fg
instead. Those pairs are verified for contrast as units — override both or
neither.