AI Suggestions
AI Suggestions is a React and Tailwind CSS component from SmoothUI, licensed MIT. Copy it and paste it into your project.
SmoothUI
MIT
other
What it is
AI Suggestions is a React and Tailwind CSS component from SmoothUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i lucide-react motion
Licence
This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
AI Suggestions
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useMemo } from "react";
const SPRING_DEFAULT = {
bounce: 0.1,
duration: 0.25,
type: "spring" as const,
};
const STAGGER_SECONDS = 0.045;
export type AISuggestion = {
id: string;
label: string;
};
export type AISuggestionsProps = {
className?: string;
/** Optional heading, e.g. "Follow-ups". */
label?: string;
onSelect?: (suggestion: AISuggestion) => void;
suggestions: AISuggestion[];
};
/**
* Distance from the middle of the row, so the stagger radiates outwards from the
* centre instead of sweeping left to right.
*
* A left-to-right sweep implies reading order and priority — that the first chip
* matters most. These are alternatives of equal weight, and radiating from the
* centre says so.
*/
const centreOutOrder = (count: number): number[] => {
const middle = (count - 1) / 2;
return Array.from({ length: count }, (_, index) => Math.abs(index - middle));
};
/**
* Prompt suggestion chips.
*
* Chips are the empty state of a chat and the follow-up row after an answer, so
* they need to arrive without feeling like a notification.
*/
const AISuggestions = ({
className,
label,
onSelect,
suggestions,
}: AISuggestionsProps) => {
const shouldReduceMotion = useReducedMotion();
const delays = useMemo(
() => centreOutOrder(suggestions.length),
[suggestions.length]
);
return (
<div className={cn("flex w-full flex-col gap-2", className)}>
{label ? (
<p className="text-muted-foreground text-xs uppercase tracking-wide">
{label}
</p>
) : null}
<ul className="flex list-none flex-wrap gap-2">
<AnimatePresence initial>
{suggestions.map((suggestion, index) => (
<motion.li
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: { opacity: 0, scale: 0.94 }
}
initial={
shouldReduceMotion
? { opacity: 1, scale: 1, y: 0 }
: { opacity: 0, scale: 0.94, y: 6 }
}
key={suggestion.id}
layout={!shouldReduceMotion}
transition={
shouldReduceMotion
? { duration: 0 }
: {
...SPRING_DEFAULT,
delay: (delays[index] ?? 0) * STAGGER_SECONDS,
}
}
>
<motion.button
className="cursor-pointer rounded-full border border-border bg-background px-3 py-1.5 text-left text-foreground text-sm transition-colors hover:border-foreground/30 hover:bg-muted"
onClick={() => onSelect?.(suggestion)}
transition={
shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT
}
type="button"
whileTap={shouldReduceMotion ? undefined : { scale: 0.97 }}
>
{suggestion.label}
</motion.button>
</motion.li>
))}
</AnimatePresence>
</ul>
</div>
);
};
export default AISuggestions;