Aura Design System

Banner

A full-width notice with an optional action and dismiss button.

Preview

Loading...
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.

Loading...
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".

Loading...
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

Loading...
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

Loading...
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/banner

Manual

Install the following dependencies:

pnpm install @radix-ui/react-icons @radix-ui/react-slot class-variance-authority

Install Button. The dismiss control uses size="icon".

pnpm dlx shadcn@latest add @aura/button

Copy 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 };