Browse the library
Components / Feedback
Urgency badge
FreeScarcity badge with a pulsing live dot and an optional percent sold bar that animates into view.
View as Markdownmotion
components/facade/urgency-badge.tsx
"use client";
import * as React from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
export type UrgencyVariant = "neutral" | "brand" | "warning";
export interface UrgencyBadgeProps {
/** Main message, e.g. "Only 3 homes left". */
label?: React.ReactNode;
/** Secondary line shown next to the progress bar, e.g. "Phase 2". */
detail?: React.ReactNode;
/** Percent sold (0 to 100). When set, a progress bar animates in. */
progress?: number;
/** Text after the percentage, defaults to "sold". */
progressSuffix?: string;
variant?: UrgencyVariant;
/** Show the pulsing live dot. */
pulse?: boolean;
className?: string;
}
const variants: Record<UrgencyVariant, { root: string; dot: string; ping: string; bar: string; track: string }> = {
neutral: {
root: "border-border bg-card text-foreground",
dot: "bg-success",
ping: "bg-success/60",
bar: "bg-foreground",
track: "bg-muted",
},
brand: {
root: "border-brand/25 bg-brand/10 text-foreground",
dot: "bg-brand",
ping: "bg-brand/60",
bar: "bg-brand",
track: "bg-brand/15",
},
warning: {
root: "border-warning/40 bg-warning/15 text-foreground",
dot: "bg-warning",
ping: "bg-warning/70",
bar: "bg-warning",
track: "bg-warning/20",
},
};
export function UrgencyBadge({
label = "Only 3 homes left",
detail,
progress,
progressSuffix = "sold",
variant = "brand",
pulse = true,
className,
}: UrgencyBadgeProps) {
const reduce = useReducedMotion();
const ref = React.useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once: true, margin: "-40px 0px" });
const v = variants[variant];
const hasProgress = typeof progress === "number";
const pct = hasProgress ? Math.min(100, Math.max(0, progress)) : 0;
return (
<div
ref={ref}
role="status"
className={cn(
"inline-flex flex-col gap-2 rounded-xl border px-3.5 py-2.5 text-sm shadow-sm",
hasProgress ? "min-w-[240px]" : "rounded-full px-3 py-1.5",
v.root,
className,
)}
>
<div className="flex items-center gap-2">
{pulse && (
<span className="relative flex size-2 shrink-0" aria-hidden="true">
{!reduce && <span className={cn("absolute inline-flex size-full animate-ping-slow rounded-full", v.ping)} />}
<span className={cn("relative inline-flex size-2 rounded-full", v.dot)} />
</span>
)}
<span className="font-medium">{label}</span>
{detail && !hasProgress && <span className="text-muted-foreground">· {detail}</span>}
</div>
{hasProgress && (
<div className="space-y-1.5">
<div
className={cn("h-1.5 w-full overflow-hidden rounded-full", v.track)}
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(pct)}
aria-label={`${Math.round(pct)}% ${progressSuffix}`}
>
<motion.div
className={cn("relative h-full rounded-full", v.bar)}
initial={{ width: reduce ? `${pct}%` : "0%" }}
animate={{ width: inView || reduce ? `${pct}%` : "0%" }}
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1], delay: 0.15 }}
>
{!reduce && (
<span
className="absolute inset-0 animate-shimmer rounded-full opacity-40"
style={{
backgroundImage: "linear-gradient(90deg, transparent 0%, var(--background) 50%, transparent 100%)",
backgroundSize: "200% 100%",
}}
aria-hidden="true"
/>
)}
</motion.div>
</div>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>
<span className="font-mono font-medium tabular-nums text-foreground">{Math.round(pct)}%</span> {progressSuffix}
</span>
{detail && <span>{detail}</span>}
</div>
</div>
)}
</div>
);
}
export default UrgencyBadge;app/page.tsx
import { UrgencyBadge } from "@/components/facade/urgency-badge";
export default function Demo() {
return (
<div className="flex w-full max-w-[640px] flex-col items-center gap-6">
<div className="flex flex-wrap items-center justify-center gap-3">
<UrgencyBadge variant="brand" label="Only 3 homes left" />
<UrgencyBadge variant="neutral" label="12 people viewed today" />
<UrgencyBadge variant="warning" label="Open house Saturday" detail="11 AM to 2 PM" />
</div>
<div className="flex flex-wrap items-start justify-center gap-4">
<UrgencyBadge variant="brand" label="Only 3 homes left in Phase 2" progress={88} detail="Mueller, Austin" />
<UrgencyBadge variant="neutral" label="Final release now open" progress={64} detail="42 of 66 residences" />
</div>
</div>
);
}Installation
$
npx shadcn@latest add https://facadeui.com/r/urgency-badge.jsonUsage
import { UrgencyBadge } from "@/components/facade/urgency-badge";
export default function Page() {
return <UrgencyBadge />;
}Props
UrgencyBadgeProps is exported, so you can extend or wrap it. Every prop has a default; <UrgencyBadge /> renders the example above.
| Prop | Type | Description |
|---|---|---|
| label | React.ReactNode | Main message, e.g. "Only 3 homes left". |
| detail | React.ReactNode | Secondary line shown next to the progress bar, e.g. "Phase 2". |
| progress | number | Percent sold (0 to 100). When set, a progress bar animates in. |
| progressSuffix | string | Text after the percentage, defaults to "sold". |
| variant | UrgencyVariant | |
| pulse | boolean | Show the pulsing live dot. |
| className | string |
Ask your agent
With the Facade MCP server connected, this prompt is enough:
Add the Facade urgency badge to this page and adapt the copy to our business.