Alert Pulse

Pulsing ring overlay for alerting canvas objects, with severity tones.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/alert-pulse.json

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

4 stories available:

Code

"use client"; import { type ComponentPropsWithoutRef, forwardRef } from "react"; import { cn } from "../../lib/utils"; /** * Severity tone of an alert pulse — drives the ring color. * * @public */ export type AlertPulseSeverity = "error" | "info" | "warn"; const SEVERITY_STROKE: Record<AlertPulseSeverity, string> = { error: "stroke-red-500", info: "stroke-blue-500", warn: "stroke-amber-500", }; const SEVERITY_FILL: Record<AlertPulseSeverity, string> = { error: "fill-red-500/20", info: "fill-blue-500/20", warn: "fill-amber-500/20", }; const SEVERITY_LABEL: Record<AlertPulseSeverity, string> = { error: "Error", info: "Info", warn: "Warning", }; /** * Localizable strings. * * @public */ export type AlertPulseLabels = { /** Override for the aria-label. Defaults to severity name. */ region?: string; }; /** * Props for {@link AlertPulse}. * * @public */ export type AlertPulseProps = { /** Center X of the pulse in canvas pixels. */ cx: number; /** Center Y of the pulse in canvas pixels. */ cy: number; /** Localizable strings. */ labels?: AlertPulseLabels; /** Outer ring radius in pixels. Defaults to `36`. */ radius?: number; /** Disable the pulse animation. Useful for snapshots. Defaults to `false`. */ reducedMotion?: boolean; /** Severity tone. Defaults to `"warn"`. */ severity?: AlertPulseSeverity; } & ComponentPropsWithoutRef<"svg">; const safeRadius = (value: number): number => (value < 6 ? 6 : value); /** * Pulsing ring overlay drawn around an alerting canvas object. The * outer ring expands and fades to communicate "attention here"; * the inner ring stays put so the object remains anchored. Pure * presentation; the host computes the center + severity from the * runtime alert stream. * * Render inside a `position: relative` parent that shares the canvas * pixel coordinate space; the SVG is `pointer-events: none` so host * gestures pass through. * * @example * ```tsx * <div className="relative h-screen w-screen"> * <Canvas /> * <AlertPulse cx={480} cy={320} severity="error" /> * </div> * ``` * * @public */ export const AlertPulse = forwardRef<SVGSVGElement, AlertPulseProps>( (props, ref) => { const { className, cx, cy, labels, radius = 36, reducedMotion = false, severity = "warn", ...rest } = props; const r = safeRadius(radius); const ariaLabel = labels?.region ?? SEVERITY_LABEL[severity]; const size = r * 2 + 24; return ( <svg aria-label={ariaLabel} className={cn( "pointer-events-none absolute z-20 overflow-visible", className, )} data-alert-pulse data-alert-severity={severity} height={size} ref={ref} role="img" style={{ left: cx - size / 2, top: cy - size / 2, }} width={size} {...rest} > <circle className={cn("origin-center", SEVERITY_STROKE[severity])} cx={size / 2} cy={size / 2} fill="none" r={r} strokeOpacity={0.7} strokeWidth={2} /> <circle className={cn( "origin-center", SEVERITY_STROKE[severity], SEVERITY_FILL[severity], reducedMotion ? null : "animate-ping", )} cx={size / 2} cy={size / 2} data-alert-pulse-ring r={r} strokeOpacity={0.4} strokeWidth={2} /> </svg> ); }, ); AlertPulse.displayName = "AlertPulse";

Dependencies

  • @vllnt/ui@^0.2.1