Milestone Reached
A milestone card rises over the page and one soft band of light crosses it — no confetti.
The animated component in this preview is rendered from the canonical file shown here. The surrounding demo shell only provides context and is not part of the copied code.
import { motion, useReducedMotion } from "motion/react";
/**
* Vibary · Milestone Reached
*
* A milestone panel arriving over the page it belongs to. The card
* rises, and one soft band of light crosses it. That is the entire
* celebration.
*
* There is deliberately no confetti here, and no particle system.
* Scattered debris is the cheapest possible way to signal delight: it
* costs the reader a second of occlusion, it cannot be made to feel
* expensive at any budget, and it plays identically whether the user
* crossed a thousand documents or their first ten. A single light pass
* scales with the moment because it reads as material quality — the
* same reason a watch face catches the light and a sticker does not.
*
* Self-contained: depends only on `motion` (react ships with your app).
* The card sits above a scrim, so it uses the CSS system colors
* `Canvas`/`CanvasText` and lands light in a light app, dark in a dark
* one; everything inside mixes from `currentColor`.
* Works with zero props; tune via `variant`, `title`, `accent`.
* Requires the automatic JSX runtime (default since React 17).
*/
export type MilestoneReachedProps = {
/** Visual character of the motion. */
variant?: "subtle" | "default" | "playful";
/** Small line above the headline. */
eyebrow?: string;
/** The milestone itself. */
title?: string;
/** One sentence of context under the headline. */
body?: string;
/** Dismiss label. */
action?: string;
/** Marker color. Semantic, so it stays literal. */
accent?: string;
/** Fires once the light has finished crossing the card. */
onComplete?: () => void;
};
type VariantConfig = {
/** How far the card travels up into place. */
rise: number;
spring: { type: "spring"; stiffness: number; damping: number };
/** Beat before the light starts, so the card is read first. */
shimmerDelay: number;
shimmerDuration: number;
/** Gap between the headline and the line under it. */
stagger: number;
};
const VARIANTS: Record<"subtle" | "default" | "playful", VariantConfig> = {
// Almost a fade. For a milestone that fires often enough to interrupt.
subtle: {
rise: 10,
spring: { type: "spring", stiffness: 480, damping: 42 },
shimmerDelay: 0.18,
shimmerDuration: 0.62,
stagger: 0.05,
},
// The all-purpose setting.
default: {
rise: 20,
spring: { type: "spring", stiffness: 380, damping: 35 },
shimmerDelay: 0.26,
shimmerDuration: 0.85,
stagger: 0.07,
},
// More travel and a slower pass of light, for a once-a-year number.
playful: {
rise: 30,
spring: { type: "spring", stiffness: 320, damping: 30 },
shimmerDelay: 0.34,
shimmerDuration: 1.05,
stagger: 0.09,
},
};
const tone = (percent: number) =>
`color-mix(in srgb, currentColor ${percent}%, transparent)`;
export default function MilestoneReached({
variant = "default",
eyebrow = "Milestone",
title = "1,000 documents",
body = "Your workspace crossed a thousand documents this morning.",
action = "View summary",
accent = "#5B8DEF",
onComplete,
}: MilestoneReachedProps) {
const reduceMotion = useReducedMotion();
const cfg = VARIANTS[variant];
const still = !!reduceMotion;
const line = (index: number) => ({
initial: still ? { opacity: 0 } : { opacity: 0, y: 7 },
animate: still ? { opacity: 1 } : { opacity: 1, y: 0 },
transition: {
delay: still ? 0.04 : 0.1 + index * cfg.stagger,
duration: 0.32,
ease: [0.22, 1, 0.36, 1] as [number, number, number, number],
},
});
return (
<div
style={{
position: "relative",
width: 320,
height: 300,
borderRadius: 18,
overflow: "hidden",
background: tone(6),
border: `1px solid ${tone(12)}`,
color: "inherit",
}}
>
{/* The page underneath, dimmed. Positioned against this box rather
than the viewport so the pattern drops into a preview; swap
`absolute` for `fixed` for an app-level moment. */}
<div style={{ position: "absolute", inset: 0, padding: 18, opacity: 0.4 }}>
{[92, 74, 84, 62].map((width, index) => (
<div
key={index}
style={{
height: 9,
width: `${width}%`,
borderRadius: 5,
background: tone(14),
marginBottom: 11,
}}
/>
))}
</div>
<motion.div
aria-hidden
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: still ? 0.15 : 0.3, ease: "easeOut" }}
style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,0.34)" }}
/>
<div
style={{
position: "absolute",
inset: 0,
display: "grid",
placeItems: "center",
padding: "18px 20px 34px",
}}
>
<motion.div
role="status"
initial={still ? { opacity: 0 } : { opacity: 0, y: cfg.rise }}
animate={{ opacity: 1, y: 0 }}
transition={
still ? { duration: 0.18, ease: "easeOut" } : { ...cfg.spring }
}
style={{
position: "relative",
width: "100%",
padding: "18px 18px 16px",
borderRadius: 16,
overflow: "hidden",
// Sits on top of the scrim, so it cannot be translucent —
// a see-through card would read as more scrim.
background: "Canvas",
color: "CanvasText",
border: `1px solid ${tone(14)}`,
boxShadow: "0 20px 46px rgba(0,0,0,0.32)",
}}
>
{/* One pass, left to right, then gone. Not a loop. The outer
element owns the animated translate so the inner one is
free to keep its static skew — the two would otherwise be
fighting over the same `transform` slot. */}
{!still && (
<motion.div
aria-hidden
initial={{ x: "-130%" }}
animate={{ x: "130%" }}
transition={{
delay: cfg.shimmerDelay,
duration: cfg.shimmerDuration,
ease: [0.4, 0, 0.2, 1],
}}
onAnimationComplete={onComplete}
style={{
position: "absolute",
top: -20,
bottom: -20,
left: 0,
width: "58%",
pointerEvents: "none",
}}
>
<div
style={{
width: "100%",
height: "100%",
transform: "skewX(-14deg)",
background: `linear-gradient(90deg, transparent, ${tone(
13
)}, transparent)`,
}}
/>
</motion.div>
)}
<motion.div
{...line(0)}
aria-hidden
style={{
display: "grid",
placeItems: "center",
width: 34,
height: 34,
borderRadius: 11,
color: accent,
background: `color-mix(in srgb, ${accent} 15%, transparent)`,
}}
>
<svg
width="18"
height="18"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5.4 17.4V3.2" />
<path d="M5.4 3.8h8.9l-1.7 3 1.7 3H5.4" />
</svg>
</motion.div>
<motion.div
{...line(1)}
style={{
marginTop: 12,
fontSize: 10.5,
fontWeight: 620,
letterSpacing: "0.09em",
textTransform: "uppercase",
color: tone(48),
}}
>
{eyebrow}
</motion.div>
<motion.div
{...line(2)}
style={{
marginTop: 3,
fontSize: 20,
fontWeight: 660,
letterSpacing: "-0.02em",
}}
>
{title}
</motion.div>
<motion.p
{...line(3)}
style={{
margin: "6px 0 0",
fontSize: 12.5,
lineHeight: 1.5,
color: tone(58),
}}
>
{body}
</motion.p>
<motion.button
{...line(4)}
type="button"
style={{
marginTop: 14,
width: "100%",
padding: "9px 12px",
borderRadius: 10,
border: `1px solid ${tone(14)}`,
background: tone(7),
color: "inherit",
font: "inherit",
fontSize: 12.5,
fontWeight: 600,
cursor: "pointer",
}}
>
{action}
</motion.button>
</motion.div>
</div>
</div>
);
}About this pattern
The celebration moment, built without scattered debris. The card rises into place on an over-damped spring, its lines arrive a few frames apart, and a single raked band of light travels across the face once and leaves. The omission is the design decision: falling particles occlude the message the user is meant to read, cannot be made to feel expensive at any budget, and play identically whether someone crossed their first ten items or their thousandth. A light pass scales with the moment because it reads as material quality rather than as decoration bolted on top. Because the card sits above a scrim it uses the CSS system colors for background and text, so it stays opaque and correctly toned in either theme.
Where it shows up
Screens we drew to show where this motion usually sits. Illustrations, not captures of any product.
- Achievements
Announcement panels that arrive with one quiet gesture and no ornament.
Related patterns
- Personal RecordThe newest column overtakes the best-ever marker, which rides up on it while the beaten mark drops away as a reference.
- Badge UnlockAn earned badge lands and one band of light crosses its face — a single pass, then still.
- Challenge CompleteA seal presses onto the finished challenge card and settles a couple of degrees off square.