banner

ui

Dismissible announcement banner with optional rainbow gradient animation. Persists dismissal in localStorage.

fumadocs/bannerv0.1.0react, next

Preview

document0 v0.4.0 is now available - check out these sweet fumadocs components!.
Introducing the plugin registry — share and discover docs components/plugins.

Installation

$npx @document0/cli add fumadocs/banner

This will also install: lucide-react@>=0.300.0, tailwind-merge@>=2.0.0

Usage

import { Banner } from "./components/fumadocs/banner";

// Example usage in your layout or page:
<Banner />

Source

After installation, this lives at components/fumadocs/banner/Banner.tsx and you can modify it however you like.

"use client";

import { type HTMLAttributes, useEffect, useState } from "react";
import { X } from "lucide-react";
import { twMerge } from "tailwind-merge";

type BannerVariant = "rainbow" | "normal";

export function Banner({
  id,
  variant = "normal",
  changeLayout = true,
  height = "3rem",
  rainbowColors = [
    "rgba(0,149,255,0.56)",
    "rgba(231,77,255,0.77)",
    "rgba(255,0,0,0.73)",
    "rgba(131,255,166,0.66)",
  ],
  ...props
}: HTMLAttributes<HTMLDivElement> & {
  height?: string;
  variant?: BannerVariant;
  rainbowColors?: string[];
  changeLayout?: boolean;
}) {
  const [open, setOpen] = useState(true);
  const globalKey = id ? `nd-banner-${id}` : null;

  useEffect(() => {
    if (globalKey) setOpen(localStorage.getItem(globalKey) !== "true");
  }, [globalKey]);

  if (!open) return null;

  return (
    <div
      id={id}
      {...props}
      className={twMerge(
        "sticky top-0 z-40 flex flex-row items-center justify-center px-4 text-center text-sm font-medium",
        variant === "normal" && "bg-secondary",
        variant === "rainbow" && "bg-background",
        !open && "hidden",
        props.className,
      )}
      style={{ height }}
    >
      {changeLayout && open ? (
        <style>
          {globalKey
            ? `:root:not(.${globalKey}) { --fd-banner-height: ${height}; }`
            : `:root { --fd-banner-height: ${height}; }`}
        </style>
      ) : null}
      {globalKey ? <style>{`.${globalKey} #${id} { display: none; }`}</style> : null}
      {globalKey ? (
        <script
          dangerouslySetInnerHTML={{
            __html: `if (localStorage.getItem('${globalKey}') === 'true') document.documentElement.classList.add('${globalKey}');`,
          }}
        />
      ) : null}

      {variant === "rainbow" ? flow({ colors: rainbowColors }) : null}
      {props.children}
      {id ? (
        <button
          type="button"
          aria-label="Close Banner"
          onClick={() => {
            setOpen(false);
            if (globalKey) localStorage.setItem(globalKey, "true");
          }}
          className="absolute end-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground/50 transition-colors hover:bg-accent hover:text-accent-foreground"
        >
          <X className="size-4" />
        </button>
      ) : null}
    </div>
  );
}

const maskImage =
  "linear-gradient(to bottom,white,transparent), radial-gradient(circle at top center, white, transparent)";

function flow({ colors }: { colors: string[] }) {
  return (
    <>
      <div
        className="absolute inset-0 z-[-1]"
        style={
          {
            maskImage,
            maskComposite: "intersect",
            animation: "fd-moving-banner 20s linear infinite",
            backgroundImage: `repeating-linear-gradient(70deg, ${[...colors, colors[0]].map((color, i) => `${color} ${(i * 50) / colors.length}%`).join(", ")})`,
            backgroundSize: "200% 100%",
            filter: "saturate(2)",
          } as object
        }
      />
      <style>
        {`@keyframes fd-moving-banner {
            from { background-position: 0% 0;  }
            to { background-position: 100% 0;  }
         }`}
      </style>
    </>
  );
}

Tags

bannerannouncementreact