Skip to content
goodtabs UI
BrowseBrowse components

Announcement bar

A full-width bar with one message, an optional arrow link and a dismiss button.

npx shadcn@latest add https://ui.goodtabs.co/r/announcement-bar.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/announcement-bar

goodtabs UI 1.0 is outSee what's new

The muted bar above a page with a message and an arrow link.

Primary

Pro launches 1 Nov, 40% off for the first weekGet Pro

The solid version for the one thing that matters this week.

Dismissible

Maintenance on Sunday 2 Nov, 02:00 to 03:00 UTCStatus page
Page content

Pass onDismiss to add the close button. The bar removes itself.

Usage

import { AnnouncementBar } from "@/components/ui/announcement-bar"

<AnnouncementBar href="/changelog" linkLabel="See what's new" onDismiss={() => setSeen(true)}>
  goodtabs UI 1.0 is out
</AnnouncementBar>

Props

PropTypeDefaultDescription
childrenReactNodeThe message. Keep it to one line.
hrefstringWhere the arrow link goes. Omit to hide the link.
linkLabelstring"Learn more"Text of the arrow link.
onDismiss() => voidAdds the dismiss button. Called after the bar hides itself.
dismissLabelstring"Dismiss announcement"Accessible name of the dismiss button.
variant"muted" | "primary""muted"muted sits quietly above a header; primary is the solid version.

Source

announcement-bar/announcement-bar.tsx
"use client";

import * as React from "react";
import { ArrowRightIcon, XIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

type AnnouncementBarProps = Omit<React.ComponentProps<"div">, "children"> & {
  /** The message. Keep it to one line. */
  children: React.ReactNode;
  /** Where the arrow link goes. */
  href?: string;
  /** Text of the arrow link. Defaults to "Learn more" when href is set. */
  linkLabel?: string;
  /** Adds the dismiss button. The bar removes itself, then calls this. */
  onDismiss?: () => void;
  /** Accessible name of the dismiss button. */
  dismissLabel?: string;
  /** "muted" sits quietly above a header; "primary" is the solid version. */
  variant?: "muted" | "primary";
};

/**
 * A full-width bar for one message, an optional arrow link and a dismiss button.
 * Dismissing hides it for this render only; persist the choice in onDismiss if it should stay gone.
 */
export function AnnouncementBar({
  children,
  href,
  linkLabel = "Learn more",
  onDismiss,
  dismissLabel = "Dismiss announcement",
  variant = "muted",
  className,
  ...props
}: AnnouncementBarProps) {
  const [dismissed, setDismissed] = React.useState(false);
  if (dismissed) return null;

  const primary = variant === "primary";

  return (
    <div
      role="region"
      aria-label="Announcement"
      data-slot="announcement-bar"
      data-variant={variant}
      className={cn(
        "relative flex min-h-10 w-full items-center justify-center gap-x-3 gap-y-0.5 px-10 py-1.5 text-center text-sm",
        primary ? "bg-primary text-primary-foreground" : "bg-muted text-foreground",
        className,
      )}
      {...props}
    >
      <div className="flex flex-wrap items-center justify-center gap-x-3 gap-y-0.5">
        <span>{children}</span>
        {href && (
          <a
            href={href}
            className={cn(
              "group/announcement-link inline-flex shrink-0 items-center gap-1 rounded-sm font-medium outline-none focus-visible:ring-2",
              primary
                ? "focus-visible:ring-primary-foreground/50"
                : "text-foreground hover:text-foreground/85 focus-visible:ring-ring/50",
            )}
          >
            {linkLabel}
            <ArrowRightIcon
              aria-hidden="true"
              className="size-3.5 transition-transform duration-150 ease-out group-hover/announcement-link:translate-x-0.5 group-focus-visible/announcement-link:translate-x-0.5 motion-reduce:transition-none motion-reduce:group-hover/announcement-link:translate-x-0"
            />
          </a>
        )}
      </div>
      {onDismiss && (
        <Button
          type="button"
          variant="ghost"
          size="icon-sm"
          aria-label={dismissLabel}
          onClick={() => {
            setDismissed(true);
            onDismiss();
          }}
          className={cn(
            "absolute top-1/2 right-1.5 -translate-y-1/2 rounded-full",
            primary &&
              "text-primary-foreground hover:bg-primary-foreground/15 hover:text-primary-foreground focus-visible:ring-primary-foreground/50",
          )}
        >
          <XIcon aria-hidden="true" />
        </Button>
      )}
    </div>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/announcement-bar/prompt.md, or the Copy AI prompt button above.

When to use

  • One launch, offer or maintenance notice above the header
  • A site-wide message that should be easy to close
  • Pointing to a changelog or status page without a modal

When not to use

  • Errors or warnings tied to a form or action, use an inline alert
  • More than one message at a time, a bar is for one thing
  • Marketing copy that changes often, people learn to ignore it

Agent notes

  • Client component; a demo or page that passes onDismiss must be a client component too
  • Dismissing only hides it for this render. Store the choice (cookie, localStorage) in onDismiss and do not render it next time
  • It has no border and no radius, so it sits flush at the top of the page or inside a frame with overflow-hidden
  • The link inherits the bar's text colour; on primary it stays primary-foreground