Banner
A full-width notice with an optional action and dismiss button.
Preview
import { Banner } from "@/components/ui/Banner";
import { Button } from "@/components/ui/Button";
export function BannerDemo() {
return (
<Banner
action={
<Button variant="pill" size="sm">
Review
</Button>
}
onDismiss={() => undefined}
>
A new color scale is ready.
</Banner>
);
}A banner is static by default: it does not set role or aria-live, so it is not announced. Pass role="status" or aria-live="polite" when the message should be announced. tone uses the same info, success, warning, and danger tokens as Alert. The message is a paragraph; use asChild when it needs to hold block content, and messageClassName to style that slot.
Mobile
Below the sm breakpoint, and whenever the banner itself is narrower than 24rem, the message and dismiss button sit on one row and the action moves below.
import { Banner } from "@/components/ui/Banner";
import { Button } from "@/components/ui/Button";
export function BannerDemoMobile() {
return (
<div className="flex w-full max-w-xs flex-col">
<Banner
action={
<Button variant="pill" size="sm">
Review
</Button>
}
onDismiss={() => undefined}
>
A new color scale is ready.
</Banner>
<Banner onDismiss={() => undefined}>A new color scale is ready.</Banner>
</div>
);
}Dismiss
Dismiss is optional. Hover paints accent step 4. Focus uses an accent step 8 ring. The control reuses Button size="icon".
import { Banner } from "@/components/ui/Banner";
import { Button } from "@/components/ui/Button";
export function BannerDemoDismissStates() {
return (
<div className="flex w-full flex-col gap-1">
<div className="flex flex-col gap-0.5">
<p className="m-0 text-sm text-gray-11">Default</p>
<Banner
action={
<Button variant="pill" size="sm">
Review
</Button>
}
onDismiss={() => undefined}
>
A new color scale is ready.
</Banner>
</div>
<div className="flex flex-col gap-0.5">
<p className="m-0 text-sm text-gray-11">Hover</p>
<Banner
dismissState="hover"
action={
<Button variant="pill" size="sm">
Review
</Button>
}
onDismiss={() => undefined}
>
A new color scale is ready.
</Banner>
</div>
<div className="flex flex-col gap-0.5">
<p className="m-0 text-sm text-gray-11">Focus</p>
<Banner
dismissState="focus"
action={
<Button variant="pill" size="sm">
Review
</Button>
}
onDismiss={() => undefined}
>
A new color scale is ready.
</Banner>
</div>
<div className="flex flex-col gap-0.5">
<p className="m-0 text-sm text-gray-11">Action only</p>
<Banner
action={
<Button variant="pill" size="sm">
Review
</Button>
}
>
A new color scale is ready.
</Banner>
</div>
<div className="flex flex-col gap-0.5">
<p className="m-0 text-sm text-gray-11">Dismiss only</p>
<Banner onDismiss={() => undefined}>A new color scale is ready.</Banner>
</div>
<div className="flex flex-col gap-0.5">
<p className="m-0 text-sm text-gray-11">Message only</p>
<Banner>A new color scale is ready.</Banner>
</div>
</div>
);
}Tones
import { Banner } from "@/components/ui/Banner";
export function BannerDemoTones() {
return (
<div className="flex w-full flex-col">
<Banner tone="info" onDismiss={() => undefined}>
A new color scale is ready.
</Banner>
<Banner tone="success" onDismiss={() => undefined}>
A new color scale is ready.
</Banner>
<Banner tone="warning" onDismiss={() => undefined}>
A new color scale is ready.
</Banner>
<Banner tone="danger" onDismiss={() => undefined}>
A new color scale is ready.
</Banner>
</div>
);
}Block message
import { Banner } from "@/components/ui/Banner";
import { Button } from "@/components/ui/Button";
export function BannerDemoRichMessage() {
return (
<Banner
asChild
action={
<Button variant="pill" size="sm">
Review
</Button>
}
onDismiss={() => undefined}
>
<div className="flex flex-col gap-0.5">
<p className="m-0 font-bold">Scale published</p>
<p className="m-0">A new color scale is ready.</p>
</div>
</Banner>
);
}Installation
Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces
pnpm dlx shadcn@latest add @aura/bannerManual
Install the following dependencies:
pnpm install @radix-ui/react-icons @radix-ui/react-slot class-variance-authorityInstall Button. The dismiss control uses size="icon".
pnpm dlx shadcn@latest add @aura/buttonCopy and paste the class names utility into your utils/class-names.ts file.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy and paste the Banner component into your components/ui/Banner.tsx file.
"use client";
import * as React from "react";
import { Cross2Icon } from "@radix-ui/react-icons";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/class-names";
import { Button } from "@/components/ui/Button";
const bannerVariants = cva("border-b", {
variants: {
tone: {
default: "border-accent-7 bg-accent-3 text-gray-12",
info: "border-info-contrast bg-info text-info-contrast",
success: "border-success-contrast bg-success text-success-contrast",
warning: "border-warning-contrast bg-warning text-warning-contrast",
danger: "border-danger-contrast bg-danger text-danger-contrast",
},
},
defaultVariants: {
tone: "default",
},
});
interface BannerProps
extends React.ComponentProps<"div">,
VariantProps<typeof bannerVariants> {
action?: React.ReactNode;
onDismiss?: () => void;
messageClassName?: string;
asChild?: boolean;
dismissState?: "default" | "hover" | "focus";
}
function Banner({
className,
children,
action,
onDismiss,
tone,
messageClassName,
asChild = false,
dismissState = "default",
...props
}: BannerProps) {
const Message = asChild ? Slot : "p";
return (
<div
{...props}
data-slot="banner"
data-tone={tone ?? "default"}
className={cn("@container w-full", bannerVariants({ tone }), className)}
>
<div
data-slot="banner-layout"
className="flex flex-col items-stretch gap-0.5 p-1 sm:flex-row sm:items-center sm:gap-1 sm:px-1.5 @max-sm:!flex-col @max-sm:!items-stretch @max-sm:!gap-0.5 @max-sm:!p-1"
>
<div
data-slot="banner-row"
className="flex w-full min-w-0 items-start gap-1 sm:contents @max-sm:!flex @max-sm:!w-full @max-sm:!items-start @max-sm:!gap-1"
>
<Message
data-slot="banner-message"
className={cn("m-0 min-w-0 flex-1 p", messageClassName)}
>
{children}
</Message>
{onDismiss ? (
<Button
type="button"
size="icon"
variant="link"
data-slot="banner-dismiss"
aria-label="Dismiss banner"
className={cn(
"shrink-0 text-inherit no-underline hover:bg-accent-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-8 sm:order-last",
dismissState === "hover" && "bg-accent-4",
dismissState === "focus" && "ring-2 ring-accent-8",
)}
onClick={onDismiss}
>
<Cross2Icon className="icon" aria-hidden />
</Button>
) : null}
</div>
{action ? (
<div data-slot="banner-action" className="w-fit shrink-0">
{action}
</div>
) : null}
</div>
</div>
);
}
export { Banner };
export type { BannerProps };