All Caught Up
A bell settles, a small badge completes its mark in one stroke, and the list confirms you are current.
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 · All Caught Up
*
* Confirmation without celebration. The bell settles, a small badge
* appears at its shoulder and completes its mark in one stroke, and the
* sentence follows. The mark is drawn in the inherited text color rather
* than a state color, and nothing bursts or flashes — being up to date
* is a normal condition, not an achievement, and an empty notification
* list that congratulates the reader wears out by the third visit.
*
* Self-contained: depends only on `react` and `motion`. Neutrals are
* mixed from the inherited text color, so it reads on light and dark
* pages alike. Works with zero props.
* Requires the automatic JSX runtime (default since React 17).
*/
export type AllCaughtUpProps = {
/** Visual character of the motion. */
variant?: "subtle" | "default" | "playful";
/** Headline of the state. */
title?: string;
/** One line saying how current the reader is. */
message?: string;
/** Small closing line, e.g. when the list was last cleared. */
footnote?: string;
/** Block width — px number or any CSS length. */
width?: number | string;
};
type VariantConfig = {
/** px the bell descends. */
drop: number;
/** Seconds the badge mark takes to complete. */
drawSeconds: number;
fadeSeconds: number;
/** Seconds before the badge appears at the bell's shoulder. */
badgeDelay: number;
bellSpring: { type: "spring"; stiffness: number; damping: number };
badgeSpring: { type: "spring"; stiffness: number; damping: number };
};
// Quality rule: two springs, both above 0.88 damping ratio
// (damping / 2√stiffness), so the bell lands once and the badge lands
// once. No swing on the bell — a ringing bell would contradict the
// sentence underneath it.
const VARIANTS: Record<"subtle" | "default" | "playful", VariantConfig> = {
// For a dropdown someone opens twenty times a day.
subtle: {
drop: 5,
drawSeconds: 0.28,
fadeSeconds: 0.22,
badgeDelay: 0.12,
bellSpring: { type: "spring", stiffness: 460, damping: 44 },
badgeSpring: { type: "spring", stiffness: 520, damping: 46 },
},
// The all-purpose setting: the stroke is legible without being slow.
default: {
drop: 8,
drawSeconds: 0.36,
fadeSeconds: 0.28,
badgeDelay: 0.16,
bellSpring: { type: "spring", stiffness: 370, damping: 37 },
badgeSpring: { type: "spring", stiffness: 420, damping: 40 },
},
// A longer arrival, for a full notifications page.
playful: {
drop: 12,
drawSeconds: 0.46,
fadeSeconds: 0.32,
badgeDelay: 0.22,
bellSpring: { type: "spring", stiffness: 300, damping: 31 },
badgeSpring: { type: "spring", stiffness: 340, damping: 33 },
},
};
/** Theme-adaptive neutral: mixing the inherited text color with
* `transparent` keeps the badge fill and the mark correct on light and
* dark pages, with no state color needed. */
const tone = (percent: number) =>
`color-mix(in srgb, currentColor ${percent}%, transparent)`;
export default function AllCaughtUp({
variant = "default",
title = "You're all caught up",
message = "Nothing new since you last looked.",
footnote = "Last update 9:41 AM",
width = 320,
}: AllCaughtUpProps) {
const reduceMotion = useReducedMotion();
const cfg = VARIANTS[variant];
const drop = reduceMotion ? 0 : cfg.drop;
const fade = (delay: number) => ({
duration: cfg.fadeSeconds,
ease: "easeOut" as const,
delay,
});
return (
<div
style={{
width,
boxSizing: "border-box",
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
padding: "28px 20px 24px",
}}
>
<motion.div
aria-hidden
initial={{ opacity: 0, y: -drop }}
animate={{ opacity: 1, y: 0 }}
transition={
reduceMotion
? { duration: 0.2, ease: "easeOut" }
: { ...cfg.bellSpring, opacity: { duration: cfg.fadeSeconds } }
}
style={{ position: "relative", lineHeight: 0, marginBottom: 14 }}
>
<BellMark />
<motion.span
initial={{ opacity: 0, scale: reduceMotion ? 1 : 0.6 }}
animate={{ opacity: 1, scale: 1 }}
transition={
reduceMotion
? { duration: 0.2, ease: "easeOut", delay: 0.1 }
: {
...cfg.badgeSpring,
delay: cfg.badgeDelay,
opacity: { duration: 0.2, delay: cfg.badgeDelay },
}
}
style={{
position: "absolute",
right: -3,
bottom: -1,
width: 20,
height: 20,
borderRadius: "50%",
display: "grid",
placeItems: "center",
background: tone(10),
border: `1px solid ${tone(16)}`,
}}
>
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
{/* One stroke, completed once. Drawn in the text color so the
state reads as a fact rather than a reward. */}
<motion.path
d="M2.6 6.2l2.3 2.4 4.5-5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.7"
initial={{ pathLength: reduceMotion ? 1 : 0 }}
animate={{ pathLength: 1 }}
transition={{
duration: reduceMotion ? 0 : cfg.drawSeconds,
ease: "easeOut",
delay: cfg.badgeDelay + 0.08,
}}
/>
</svg>
</motion.span>
</motion.div>
<motion.div
initial={{ opacity: 0, y: drop * 0.6 }}
animate={{ opacity: 1, y: 0 }}
transition={fade(cfg.badgeDelay + 0.14)}
style={{ fontSize: 15, fontWeight: 640 }}
>
{title}
</motion.div>
<motion.div
initial={{ opacity: 0, y: drop * 0.6 }}
animate={{ opacity: 0.58, y: 0 }}
transition={fade(cfg.badgeDelay + 0.2)}
style={{ fontSize: 12.5, marginTop: 5 }}
>
{message}
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.36 }}
transition={fade(cfg.badgeDelay + 0.28)}
style={{ fontSize: 11, marginTop: 12 }}
>
{footnote}
</motion.div>
</div>
);
}
/** Line art authored inline: a bell at rest — no clapper swing, no tilt.
* Stroked in `currentColor` so it inherits the page theme. */
function BellMark() {
return (
<svg width="46" height="48" viewBox="0 0 46 48" fill="none" aria-hidden>
<g
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.32"
>
<path d="M11.5 31.5c2-2 2.6-4 2.6-7.5v-2.5a9 9 0 0 1 18 0V24c0 3.5.6 5.5 2.6 7.5z" />
<path d="M19.5 36a3.6 3.6 0 0 0 7 0" />
<path d="M23 11.5V8" />
</g>
</svg>
);
}About this pattern
Confirmation without celebration. The bell descends and settles once — no swing, because a ringing bell would contradict the sentence underneath it — then a badge appears at its shoulder and completes its mark in a single stroke. The mark is drawn in the inherited text color rather than a state color, and nothing bursts, flashes or scales: being up to date is an ordinary condition, and an empty notification list that congratulates the reader wears thin by the third visit that day. Reduced motion shows the completed badge with no stroke and no travel.
Where it shows up
Screens we drew to show where this motion usually sits. Illustrations, not captures of any product.
- Notification center
An emptied notification inbox shows a small mark and a line saying there is nothing left.
Related patterns
- Empty CartThe cart mark drops and settles once, then recently viewed items slide in as the way back to shopping.
- Error RecoveryA failed panel settles without alarm, and the retry glyph turns exactly once per attempt.
- No ConnectionTwo halves of a broken link stand apart, then close the gap in one settle when the attempt lands.