Feedback
Banner
A page-top announcement bar with info, accent and destructive variants, a real action link and consumer-owned dismissal.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/banner.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "Banner" component using
class-variance-authority (cva) and lucide-react icons.
Contract
- Export a forwardRef div extending React.HTMLAttributes<HTMLDivElement> plus
VariantProps<typeof bannerVariants>; spread remaining props on the root and
merge className via cn().
- Props: variant = "info" | "accent" | "destructive" (default "info");
icon?: ReactNode (per-variant defaults: Info / Sparkles / TriangleAlert;
pass null to render no icon); action?: { label: string; href: string };
onDismiss?: () => void; children is the message copy.
- The banner never hides itself: onDismiss only reports intent. Visibility is
consumer-owned — the parent conditionally renders the banner (useState,
optionally persisted to localStorage).
Behavior
- One full-width flex row: icon → flex-1 message → action link → dismiss
button. The message span is min-w-0 flex-1 so long copy wraps inside it;
action and dismiss are shrink-0 (action also whitespace-nowrap) so the
controls stay pinned right and never break mid-label.
- action renders a real <a href> with a trailing ArrowRight icon — no onClick
simulation, so middle-click and open-in-new-tab keep working.
- The dismiss button renders only when onDismiss is provided: type="button",
aria-label "Dismiss", an X icon, calls onDismiss on click.
- Live-region semantics follow the variant: info and accent get role="status"
(polite), destructive gets role="alert" so screen readers announce urgent
banners immediately when they mount.
Rendering & styling
- Semantic tokens only, no hardcoded colors. cva variants:
info = bg-muted text-foreground; accent = bg-primary/10 border-primary/30
with the icon and action tinted text-primary;
destructive = bg-destructive/10 text-destructive. The base keeps a
transparent border so every variant shares one box height.
- Icon wrapper, ArrowRight and X are aria-hidden (decorative); the action link
and dismiss button both carry focus-visible:ring-2 ring-ring; the action
underlines on hover, the dismiss button uses an opacity hover transition.
- No animation, so no reduced-motion branch is needed.
Customization levers
- Sticky top bar: wrap the banner in a `sticky top-0 z-50` container in your
layout shell (add `border-b`, and rounded-none if you themed a radius); the
component itself stays position-agnostic.
- Dismiss + remember: pair onDismiss with consumer state, e.g.
`const [hidden, setHidden] = useState(false)` and
`onDismiss={() => { setHidden(true); localStorage.setItem("banner-2026-07", "1") }}`,
initializing hidden from that key on mount (guard with `typeof window` in
SSR frameworks). Key the entry per campaign so a new announcement reappears
after an old one was dismissed.
- Scrolling announcements: children is a plain flex-1 slot — drop a marquee
component inside it for multi-message ticker bars, keeping action and
dismiss outside the marquee so they stay static and clickable.
- Add a variant (e.g. success): one cva entry mapping to a token pair
(background at /10, matching text token or var(--chart-N) via color-mix)
plus one entry in the default-icon map — layout and roles are untouched.
- Density: px-4 py-3 text-sm is the only sizing; pass py-2 text-xs via
className for a compact strip, or py-4 for a hero-adjacent bar.
- Drop the icon with icon={null}, or brand the bar by passing a logo node.Concepts
- Consumer-owned dismissal — the X button only reports intent via
onDismiss; the parent owns visibility (state, localStorage, per-campaign keys), so persistence policy never leaks into the component. - Variant-driven live region — the same markup switches between
role="status"(polite: info, accent) androle="alert"(assertive: destructive), so urgency is an accessibility semantic, not just a color. - Default icon per variant — each variant ships a recognizable glyph out of the box; the
iconprop overrides it andicon={null}removes it, keeping the common case zero-config. - Real anchor action — the action is an
<a href>, not a styled div with onClick, so keyboard focus, middle-click and open-in-new-tab behave like every other link. - Single-row resilience — the message lives in a
min-w-0 flex-1span that absorbs wrapping, while icon, action and dismiss areshrink-0; long copy never pushes the controls off-screen.