feedbackclient

banner

a wide announcement strip, built just like a primary button.

blurrow v1.0 is out. read the release notes.

installation

new to blurrow? this sets up your project in one go: the package, the styles and the font. already done it? skip straight to usage. the installation page has the details.

bash
npx @blurrow/ui init
›rather own the code? copy the source instead

let the cli copy it, along with anything it needs:

bash
npx @blurrow/ui add banner

or by hand: drop these files into components/ui and import from there. banner leans on CloseButton.tsx, so those come along too.

components/ui/Banner.tsxtsx
"use client";
import { ReactNode } from "react";
import { CloseButton } from "./CloseButton";
type BannerVariant = "default" | "accent" | "warning";
/* A banner is a dark bezel holding a dark face — the same build as the
primary button, stretched to full width. */
const variantStyles: Record<
BannerVariant,
{ bezel: string; bezelShadow: string; face: string; faceShadow: string; text: string; tone: "light" | "dark" }
> = {
default: {
bezel: "var(--bezel-dark)",
bezelShadow: "var(--bezel-dark-shadow)",
face: "var(--face-dark)",
faceShadow: "var(--face-dark-shadow)",
text: "text-white",
tone: "dark",
},
accent: {
bezel: "linear-gradient(180deg, #4c2a99 0%, #2e1065 100%)",
bezelShadow:
"0 0 0 0.5px rgba(46,16,101,0.36), 0 4px 12px -4px rgba(76,29,149,0.45), inset 0 2px 3px -2px rgba(255,255,255,0.09)",
face: "linear-gradient(180deg, #7c3aed 0%, #6d28d9 100%)",
faceShadow:
"0 0 3px rgba(46,16,101,0.35), 0 1px 2px rgba(46,16,101,0.4), inset 0 2px 3px -2px rgba(255,255,255,0.165)",
text: "text-white",
tone: "dark",
},
warning: {
bezel: "linear-gradient(180deg, #fde68a 0%, #f5c84b 100%)",
bezelShadow:
"0 0 0 0.5px rgba(180,83,9,0.15), 0 4px 12px -4px rgba(217,119,6,0.3), inset 0 2px 3px -2px rgba(255,255,255,0.45)",
face: "linear-gradient(180deg, #fffbeb 0%, #fef3c7 100%)",
faceShadow:
"0 0 3px rgba(180,83,9,0.255), 0 1px 2px rgba(180,83,9,0.12), inset 0 2px 3px -2px rgba(255,255,255,1)",
text: "text-amber-900",
tone: "light",
},
};
export function Banner({
variant = "default",
children,
icon,
dismissible = false,
onDismiss,
action,
className = "",
}: {
variant?: BannerVariant;
children: ReactNode;
icon?: ReactNode;
dismissible?: boolean;
onDismiss?: () => void;
action?: ReactNode;
className?: string;
}) {
const s = variantStyles[variant];
return (
<div
role="region"
aria-label="Announcement"
className={`w-full rounded-[18px] p-1 ${className}`}
style={{ background: s.bezel, boxShadow: s.bezelShadow }}
>
<div
className={`rounded-[14px] pl-4 pr-2 py-2.5 flex items-center gap-3 ${s.text}`}
style={{ background: s.face, boxShadow: s.faceShadow }}
>
{icon && <span aria-hidden className="flex-shrink-0 opacity-80">{icon}</span>}
{/* Left-aligned so it reads in one sweep with the icon beside it */}
<span className="text-[13px] font-medium flex-1 min-w-0 leading-relaxed">
{children}
</span>
{action && <span className="flex-shrink-0">{action}</span>}
{dismissible && (
<CloseButton size="sm" tone={s.tone} label="Dismiss announcement" onClick={onDismiss} />
)}
</div>
</div>
);
}
components/ui/CloseButton.tsxtsx
"use client";
import React from "react";
interface CloseButtonProps {
onClick?: () => void;
label?: string;
size?: "sm" | "md";
tone?: "light" | "dark";
className?: string;
}
const sizes = {
sm: { box: "w-6 h-6 rounded-[7px]", icon: 9 },
md: { box: "w-7 h-7 rounded-[8px]", icon: 10 },
};
/* Quiet until hovered, then it rises into a raised face like every other
pressable thing — so it reads as a control, not decoration. */
export function CloseButton({
onClick,
label = "Close",
size = "md",
tone = "light",
className = "",
}: CloseButtonProps) {
const dark = tone === "dark";
const s = sizes[size];
return (
<button
type="button"
onClick={onClick}
aria-label={label}
className={[
"group/close relative shrink-0 flex items-center justify-center cursor-pointer",
"transition-[color,background,scale] duration-150 active:scale-95",
"focus-visible:outline-2 focus-visible:outline-offset-2",
dark
? "text-white/45 hover:text-white focus-visible:outline-white"
: "text-neutral-400 hover:text-neutral-900 focus-visible:outline-neutral-900",
s.box,
className,
].join(" ")}
>
<span
aria-hidden
className={[
"absolute inset-0 rounded-[inherit] opacity-0 group-hover/close:opacity-100 transition-opacity duration-150",
dark
? "[background:var(--tile-dark)] [box-shadow:var(--tile-dark-shadow)]"
: "[background:var(--face-light)] [box-shadow:var(--face-light-shadow)]",
].join(" ")}
/>
<svg
className="relative"
width={s.icon}
height={s.icon}
fill="none"
stroke="currentColor"
strokeWidth="2.5"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
);
}

usage

tsx
import { Banner } from "@blurrow/ui";
tsx
<Banner dismissible>blurrow v1.0 is out.</Banner>

examples

default banner for announcements.
accent banner for launches and promotions.
warning banner for scheduled maintenance.
pro is 20% off this week.