feedback

alert

a message that sits in the page, with an icon tile that changes per variant.

new version available

blurrow v1.1.0 is ready to install.

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 alert

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

components/ui/Alert.tsxtsx
import React from "react";
import { CloseButton } from "./CloseButton";
type AlertVariant = "default" | "success" | "warning" | "error";
interface AlertProps {
variant?: AlertVariant;
title: string;
description?: string;
onDismiss?: () => void;
className?: string;
}
/* Tiles share Toast's lit-from-above gradients, so an alert and the toast
it came from look like the same object. */
const variantConfig: Record<AlertVariant, {
tile: string;
icon: React.ReactNode;
}> = {
default: {
tile: "var(--face-dark)",
icon: (
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.75" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<path strokeLinecap="round" d="M12 8v4m0 4h.01" />
</svg>
),
},
success: {
tile: "linear-gradient(180deg, #10b981 0%, #059669 100%)",
icon: (
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
),
},
warning: {
tile: "linear-gradient(180deg, #fbbf24 0%, #f59e0b 100%)",
icon: (
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.75" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
</svg>
),
},
error: {
tile: "linear-gradient(180deg, #f87171 0%, #dc2626 100%)",
icon: (
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
),
},
};
export function Alert({ variant = "default", title, description, onDismiss, className = "" }: AlertProps) {
const { tile, icon } = variantConfig[variant];
return (
<div
// Errors and warnings interrupt; everything else waits its turn
role={variant === "error" || variant === "warning" ? "alert" : "status"}
className={["rounded-[18px] p-1.5", className].join(" ")}
style={{ background: "var(--panel-bezel)", boxShadow: "var(--panel-shadow)" }}
>
<div
className="bg-white rounded-[13px] pl-3 pr-2 py-3 flex items-start gap-3"
style={{ boxShadow: "var(--panel-face-shadow)" }}
>
<div
aria-hidden
className="shrink-0 w-7 h-7 rounded-[8px] flex items-center justify-center text-white"
style={{
background: tile,
boxShadow:
"0 0 3px rgba(0,0,0,0.212), 0 1px 2px rgba(0,0,0,0.15), inset 0 2px 3px -2px rgba(255,255,255,0.188)",
}}
>
{icon}
</div>
<div className="flex-1 min-w-0 pt-[5px]">
<p className="text-[13px] font-semibold text-neutral-900 leading-tight tracking-[-0.01em]">{title}</p>
{description && (
<p className="text-[12.5px] text-neutral-500 mt-1 leading-relaxed">{description}</p>
)}
</div>
{onDismiss && <CloseButton size="sm" label="Dismiss" 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 { Alert } from "@blurrow/ui";
tsx
<Alert variant="success" title="published" description="your post is live." />

examples

variants

new version available

blurrow v1.1.0 is ready to install.

published successfully

your post is now live and indexed.

dismissible

props

straight from the source, so they're always up to date.

propsts
type AlertVariant = "default" | "success" | "warning" | "error";
interface AlertProps {
variant?: AlertVariant;
title: string;
description?: string;
onDismiss?: () => void;
className?: string;
}