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 banneror 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
1"use client";2
3import { ReactNode } from "react";4import { CloseButton } from "./CloseButton";5
6type BannerVariant = "default" | "accent" | "warning";7
8/* A banner is a dark bezel holding a dark face — the same build as the9 primary button, stretched to full width. */10const variantStyles: Record<11 BannerVariant,12 { bezel: string; bezelShadow: string; face: string; faceShadow: string; text: string; tone: "light" | "dark" }13> = {14 default: {15 bezel: "var(--bezel-dark)",16 bezelShadow: "var(--bezel-dark-shadow)",17 face: "var(--face-dark)",18 faceShadow: "var(--face-dark-shadow)",19 text: "text-white",20 tone: "dark",21 },22 accent: {23 bezel: "linear-gradient(180deg, #4c2a99 0%, #2e1065 100%)",24 bezelShadow:25 "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)",26 face: "linear-gradient(180deg, #7c3aed 0%, #6d28d9 100%)",27 faceShadow:28 "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)",29 text: "text-white",30 tone: "dark",31 },32 warning: {33 bezel: "linear-gradient(180deg, #fde68a 0%, #f5c84b 100%)",34 bezelShadow:35 "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)",36 face: "linear-gradient(180deg, #fffbeb 0%, #fef3c7 100%)",37 faceShadow:38 "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)",39 text: "text-amber-900",40 tone: "light",41 },42};43
44export function Banner({45 variant = "default",46 children,47 icon,48 dismissible = false,49 onDismiss,50 action,51 className = "",52}: {53 variant?: BannerVariant;54 children: ReactNode;55 icon?: ReactNode;56 dismissible?: boolean;57 onDismiss?: () => void;58 action?: ReactNode;59 className?: string;60}) {61 const s = variantStyles[variant];62
63 return (64 <div65 role="region"66 aria-label="Announcement"67 className={`w-full rounded-[18px] p-1 ${className}`}68 style={{ background: s.bezel, boxShadow: s.bezelShadow }}69 >70 <div71 className={`rounded-[14px] pl-4 pr-2 py-2.5 flex items-center gap-3 ${s.text}`}72 style={{ background: s.face, boxShadow: s.faceShadow }}73 >74 {icon && <span aria-hidden className="flex-shrink-0 opacity-80">{icon}</span>}75
76 {/* Left-aligned so it reads in one sweep with the icon beside it */}77 <span className="text-[13px] font-medium flex-1 min-w-0 leading-relaxed">78 {children}79 </span>80
81 {action && <span className="flex-shrink-0">{action}</span>}82
83 {dismissible && (84 <CloseButton size="sm" tone={s.tone} label="Dismiss announcement" onClick={onDismiss} />85 )}86 </div>87 </div>88 );89}components/ui/CloseButton.tsxtsx
1"use client";2
3import React from "react";4
5interface CloseButtonProps {6 onClick?: () => void;7 label?: string;8 size?: "sm" | "md";9 tone?: "light" | "dark";10 className?: string;11}12
13const sizes = {14 sm: { box: "w-6 h-6 rounded-[7px]", icon: 9 },15 md: { box: "w-7 h-7 rounded-[8px]", icon: 10 },16};17
18/* Quiet until hovered, then it rises into a raised face like every other19 pressable thing — so it reads as a control, not decoration. */20export function CloseButton({21 onClick,22 label = "Close",23 size = "md",24 tone = "light",25 className = "",26}: CloseButtonProps) {27 const dark = tone === "dark";28 const s = sizes[size];29
30 return (31 <button32 type="button"33 onClick={onClick}34 aria-label={label}35 className={[36 "group/close relative shrink-0 flex items-center justify-center cursor-pointer",37 "transition-[color,background,scale] duration-150 active:scale-95",38 "focus-visible:outline-2 focus-visible:outline-offset-2",39 dark40 ? "text-white/45 hover:text-white focus-visible:outline-white"41 : "text-neutral-400 hover:text-neutral-900 focus-visible:outline-neutral-900",42 s.box,43 className,44 ].join(" ")}45 >46 <span47 aria-hidden48 className={[49 "absolute inset-0 rounded-[inherit] opacity-0 group-hover/close:opacity-100 transition-opacity duration-150",50 dark51 ? "[background:var(--tile-dark)] [box-shadow:var(--tile-dark-shadow)]"52 : "[background:var(--face-light)] [box-shadow:var(--face-light-shadow)]",53 ].join(" ")}54 />55 <svg56 className="relative"57 width={s.icon}58 height={s.icon}59 fill="none"60 stroke="currentColor"61 strokeWidth="2.5"62 viewBox="0 0 24 24"63 >64 <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />65 </svg>66 </button>67 );68}usage
tsx
import { Banner } from "@blurrow/ui";tsx
<Banner dismissible>blurrow v1.0 is out.</Banner>examples
variants
default banner for announcements.
accent banner for launches and promotions.
warning banner for scheduled maintenance.
with action
pro is 20% off this week.