Suggestion Chips Cascade
Suggested next actions land in quick succession under the answer, one chip after another.
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, type Variants } from "motion/react";
/**
* Vibary · Suggestion Chips Cascade
*
* AI-suggested next actions landing in quick succession under a
* response, one chip after another.
*
* Self-contained: depends only on `motion` (react ships with your app).
* Works with zero props; tune via `variant`, `items`, `onSelect`.
* Requires the automatic JSX runtime (default since React 17).
*/
export type SuggestionChipsCascadeProps = {
/** Visual character of the motion. */
variant?: "subtle" | "default" | "playful";
/** Chip labels, in the order they should land. */
items?: string[];
/** Called with the chosen suggestion. */
onSelect?: (item: string, index: number) => void;
/** Accessible name for the group of suggestions. */
label?: string;
};
type VariantConfig = {
/** Gap between chips — small enough to read as one cascade, not a queue. */
stagger: number;
/** Beat after the answer before the first chip lands. */
delay: number;
/** px a chip travels up as it lands. */
riseY: number;
spring: { type: "spring"; stiffness: number; damping: number };
};
// Damping ratios (ζ = damping / 2√stiffness) stay at or above 0.8: chips
// are mostly text, and a row of five bouncing labels is the cheapest
// looking thing an AI surface can do. Energy comes from cadence instead.
const VARIANTS: Record<"subtle" | "default" | "playful", VariantConfig> = {
// ζ ≈ 1.07 — no overshoot, minimal travel. For chips that show on every turn.
subtle: {
stagger: 0.04,
delay: 0.06,
riseY: 5,
spring: { type: "spring", stiffness: 460, damping: 46 },
},
// ζ ≈ 0.93 — lands clean. The all-purpose setting.
default: {
stagger: 0.055,
delay: 0.08,
riseY: 9,
spring: { type: "spring", stiffness: 420, damping: 38 },
},
// ζ ≈ 0.82 — one soft settle, longer travel, quicker succession.
playful: {
stagger: 0.07,
delay: 0.1,
riseY: 14,
spring: { type: "spring", stiffness: 380, damping: 32 },
},
};
const SAMPLE_ITEMS = [
"Summarize the thread",
"Draft a reply",
"Create a task",
"Export as PDF",
];
export default function SuggestionChipsCascade({
variant = "default",
items = SAMPLE_ITEMS,
onSelect,
label = "Suggested actions",
}: SuggestionChipsCascadeProps) {
const reduceMotion = useReducedMotion();
const cfg = VARIANTS[variant];
// Reduced motion: the chips arrive together on one short fade. Order
// is carried by the DOM, which is what assistive tech reads anyway.
const listVariants: Variants = reduceMotion
? { hidden: {}, shown: {} }
: {
hidden: {},
shown: {
transition: {
delayChildren: cfg.delay,
staggerChildren: cfg.stagger,
},
},
};
const chipVariants: Variants = reduceMotion
? {
hidden: { opacity: 0 },
shown: { opacity: 1, transition: { duration: 0.2, ease: "easeOut" } },
}
: {
hidden: { opacity: 0, y: cfg.riseY },
shown: {
opacity: 1,
y: 0,
transition: {
y: cfg.spring,
// Opacity on its own quick curve; springing a fade looks muddy.
opacity: { duration: 0.18, ease: "easeOut" },
},
},
};
return (
<motion.div
role="group"
aria-label={label}
variants={listVariants}
initial="hidden"
animate="shown"
style={{ display: "flex", flexWrap: "wrap", gap: 8 }}
>
{items.map((item, index) => (
<motion.button
key={item}
type="button"
variants={chipVariants}
// A 1px press instead of a scale: the chip is a text label, and
// squashing type is the thing this pattern must never do.
whileTap={reduceMotion ? undefined : { y: 1 }}
onClick={() => onSelect?.(item, index)}
style={{
padding: "7px 12px",
borderRadius: 999,
border: "1px solid rgba(127,127,140,0.28)",
background: "rgba(127,127,140,0.10)",
color: "inherit",
font: "inherit",
fontSize: 13,
lineHeight: 1.2,
cursor: "pointer",
}}
>
{item}
</motion.button>
))}
</motion.div>
);
}About this pattern
What the assistant offers to do next: draft a reply, file a ticket, export the summary. The chips arrive in a fast cascade so the eye tracks along the row instead of meeting a wall of buttons at once, and arriving after the answer marks them as an offer rather than a demand. Each chip rises and fades on its own short spring — never a scale, because a chip is mostly a text label and squashed type reads as cheap.
Where it shows up
Screens we drew to show where this motion usually sits. Illustrations, not captures of any product.
- AI assistant
Follow-up prompts presented under a finished response.
Related patterns
- Autocomplete RiseA completion panel lifts into place under the field, its rows landing a beat apart.
- Prompt Template FillPicking a starter writes its outline into the composer with the blanks lighting up.
- Prompt Submit LiftThe composed prompt lifts out of the field and docks as a sent message while the thread makes room.