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…

"use client"

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { ArrowRight, Info, Sparkles, TriangleAlert, X } from "lucide-react"
import { cn } from "@/lib/utils"

const bannerVariants = cva(
  "relative flex w-full items-center gap-3 border px-4 py-3 text-sm",
  {
    variants: {
      variant: {
        info: "border-transparent bg-muted text-foreground",
        accent: "border-primary/30 bg-primary/10 text-foreground",

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/banner.json

Prompt

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) and role="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 icon prop overrides it and icon={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-1 span that absorbs wrapping, while icon, action and dismiss are shrink-0; long copy never pushes the controls off-screen.

On This Page