Clickable prompt suggestion chips that guide users toward common queries. Built on shadcn's Button with a composable compound pattern.
"use client";
import {
Suggestions,
SuggestionList,
Suggestion,
} from "@/components/nexus-ui/suggestions";
export default function SuggestionDefault() {
return (
<Suggestions onSelect={(value) => console.log(value)}>
<SuggestionList className="justify-center max-w-lg">
<Suggestion>What is AI?</Suggestion>
<Suggestion>Teach me Engineering from scratch</Suggestion>
<Suggestion>Design a weekly workout plan</Suggestion>
<Suggestion>Places to visit in France</Suggestion>
<Suggestion>How to learn React?</Suggestion>
</SuggestionList>
</Suggestions>
);
}
Installation
npx shadcn@latest add @nexus-ui/suggestionspnpm dlx shadcn@latest add @nexus-ui/suggestionsyarn dlx shadcn@latest add @nexus-ui/suggestionsbunx shadcn@latest add @nexus-ui/suggestionsCopy and paste the following code into your project.
"use client";
import * as React from "react";
import { Presence } from "@radix-ui/react-presence";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
const suggestionVariants = cva(
"h-8 gap-1.5 rounded-full px-4 text-sm font-normal shadow-none outline-0 transition-all duration-150 focus-visible:ring-2 focus-visible:ring-ring active:scale-99",
{
variants: {
variant: {
filled:
"border-none bg-muted text-primary hover:bg-border",
outline:
"border border-input bg-transparent text-primary hover:bg-muted",
ghost:
"border-none bg-transparent text-muted-foreground hover:bg-muted hover:text-primary",
},
},
defaultVariants: {
variant: "filled",
},
},
);
type SuggestionsContextValue = {
onSelect?: (value: string) => void;
};
const SuggestionsContext = React.createContext<SuggestionsContextValue>({});
type SuggestionsProps = Omit<
React.HTMLAttributes<HTMLDivElement>,
"onSelect"
> & {
onSelect?: (value: string) => void;
};
function Suggestions({ className, onSelect, ...props }: SuggestionsProps) {
return (
<SuggestionsContext.Provider value={{ onSelect }}>
<div
data-slot="suggestions"
role="group"
aria-label="Suggestions"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
</SuggestionsContext.Provider>
);
}
type SuggestionListProps = React.HTMLAttributes<HTMLDivElement> & {
orientation?: "horizontal" | "vertical";
};
function SuggestionList({
className,
orientation = "horizontal",
...props
}: SuggestionListProps) {
return (
<div
data-slot="suggestion-list"
role="group"
aria-label="Suggestions"
className={cn(
"flex animate-in gap-2 duration-150 fade-in-0",
orientation === "horizontal"
? "flex-row flex-wrap items-center justify-center"
: "flex-col items-start",
className,
)}
{...props}
/>
);
}
type SuggestionProps = Omit<React.ComponentProps<typeof Button>, "variant"> &
VariantProps<typeof suggestionVariants> & {
value?: string;
highlight?: string | string[];
};
function highlightText(
text: string,
terms: string | string[],
): React.ReactNode {
const termList = Array.isArray(terms) ? terms : [terms];
const escaped = termList.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
const pattern = new RegExp(`(${escaped.join("|")})`, "gi");
const parts = text.split(pattern);
return (
<span>
{parts.map((part, i) =>
escaped.some((e) => new RegExp(`^${e}$`, "i").test(part)) ? (
<span key={i} className="text-muted-foreground">
{part}
</span>
) : (
<span key={i} className="text-secondary-foreground">
{part}
</span>
),
)}
</span>
);
}
function Suggestion({
className,
value,
variant = "filled",
highlight,
onClick,
children,
...props
}: SuggestionProps) {
const { onSelect } = React.useContext(SuggestionsContext);
const textToHighlight =
typeof children === "string" ? children : (value ?? "");
const nonStringChildren = React.Children.toArray(children).filter(
(c) => typeof c !== "string",
);
const rendered =
highlight && textToHighlight ? (
<>
{highlightText(textToHighlight, highlight)}
{nonStringChildren}
</>
) : (
children
);
return (
<Button
data-slot="suggestion"
className={cn(suggestionVariants({ variant }), className)}
onClick={(e) => {
onClick?.(e);
const text = value ?? (typeof children === "string" ? children : "");
if (text && onSelect) onSelect(text);
}}
{...props}
>
{rendered}
</Button>
);
}
const FOCUSABLE =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
function getFocusableElements(container: HTMLElement): HTMLElement[] {
return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE));
}
const SuggestionPanelContext = React.createContext<{
onOpenChange: (open: boolean) => void;
} | null>(null);
type SuggestionPanelProps = React.ComponentProps<"div"> & {
open?: boolean;
onOpenChange?: (open: boolean) => void;
onClose?: () => void;
};
function SuggestionPanel({
className,
open = true,
onOpenChange,
onClose,
ref,
children,
...props
}: SuggestionPanelProps) {
const panelRef = React.useRef<HTMLDivElement>(null);
const mergedRef = React.useMemo(
() => (node: HTMLDivElement | null) => {
(panelRef as React.MutableRefObject<HTMLDivElement | null>).current =
node;
if (typeof ref === "function") ref(node);
else if (ref)
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
},
[ref],
);
const handleOpenChange = React.useCallback(
(next: boolean) => {
onOpenChange?.(next);
},
[onOpenChange],
);
const handleAnimationEnd = React.useCallback(
(e: React.AnimationEvent) => {
if (e.animationName === "exit" && !open) onClose?.();
},
[open, onClose],
);
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") handleOpenChange(false);
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [handleOpenChange]);
React.useEffect(() => {
if (!open) return;
const panel = panelRef.current;
if (!panel) return;
const focusable = getFocusableElements(panel);
if (focusable.length > 0) focusable[0].focus();
}, [open]);
React.useEffect(() => {
const panel = panelRef.current;
if (!panel) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== "Tab") return;
const focusable = getFocusableElements(panel);
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey) {
if (active === first) {
e.preventDefault();
last.focus();
}
} else {
if (active === last) {
e.preventDefault();
first.focus();
}
}
};
panel.addEventListener("keydown", handleKeyDown);
return () => panel.removeEventListener("keydown", handleKeyDown);
}, []);
const ctx = React.useMemo(
() => ({ onOpenChange: handleOpenChange }),
[handleOpenChange],
);
return (
<Presence present={open}>
<div
ref={mergedRef}
data-slot="suggestion-panel"
role="dialog"
aria-modal="true"
aria-label="Suggestions panel"
data-state={open ? "open" : "closed"}
onAnimationEnd={handleAnimationEnd}
className={cn(
"rounded-t-0 absolute inset-x-0 -top-7.5 z-0 mx-auto flex w-[calc(100%-16px)] flex-col items-center justify-center gap-3 rounded-b-[20px] bg-muted px-2 py-3 duration-200 data-[state=closed]:animate-out data-[state=closed]:duration-0 data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-top-2",
className,
)}
{...props}
>
<SuggestionPanelContext.Provider value={ctx}>
{children}
</SuggestionPanelContext.Provider>
</div>
</Presence>
);
}
function SuggestionPanelHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
data-slot="suggestion-panel-header"
className={cn("flex w-full items-center justify-between px-3", className)}
{...props}
/>
);
}
function SuggestionPanelTitle({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
data-slot="suggestion-panel-title"
className={cn("flex items-center gap-1.5", className)}
{...props}
/>
);
}
type SuggestionPanelCloseProps =
React.ButtonHTMLAttributes<HTMLButtonElement> & {
asChild?: boolean;
};
function SuggestionPanelClose({
asChild = false,
className,
onClick,
"aria-label": _ariaLabel,
...props
}: SuggestionPanelCloseProps) {
const ctx = React.useContext(SuggestionPanelContext);
const Comp = asChild ? Slot : "button";
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
ctx?.onOpenChange(false);
onClick?.(e);
};
return (
<Comp
type={asChild ? undefined : "button"}
data-slot="suggestion-panel-close"
aria-label="Close suggestions panel"
className={cn(
"flex cursor-pointer items-center justify-center text-muted-foreground hover:text-primary dark:hover:text-primary",
className,
)}
onClick={handleClick}
{...props}
/>
);
}
type SuggestionPanelContentProps = React.HTMLAttributes<HTMLDivElement> & {
asChild?: boolean;
};
function SuggestionPanelContent({
asChild = false,
className,
...props
}: SuggestionPanelContentProps) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="suggestion-panel-content"
className={cn("w-full", className)}
{...props}
/>
);
}
export {
Suggestions,
SuggestionList,
Suggestion,
SuggestionPanel,
SuggestionPanelHeader,
SuggestionPanelTitle,
SuggestionPanelClose,
SuggestionPanelContent,
};
export default Suggestions;
Update the import paths to match your project setup.
Usage
import {
Suggestions,
SuggestionList,
Suggestion,
} from "@/components/nexus-ui/suggestions";<Suggestions onSelect={(value) => handleSuggestion(value)}>
<SuggestionList>
<Suggestion>Tell me a joke</Suggestion>
<Suggestion>Explain quantum computing</Suggestion>
</SuggestionList>
</Suggestions>Examples
Variants
The Suggestion component supports three variants: filled (filled background), outline (bordered), and ghost (transparent until hovered).
"use client";
import {
Suggestions,
SuggestionList,
Suggestion,
} from "@/components/nexus-ui/suggestions";
export default function SuggestionVariants() {
return (
<Suggestions onSelect={(value) => console.log(value)}>
<SuggestionList>
<Suggestion>Filled</Suggestion>
<Suggestion variant="outline">Outline</Suggestion>
<Suggestion variant="ghost">Ghost</Suggestion>
</SuggestionList>
</Suggestions>
);
}
Vertical Layout
Use orientation="vertical" on SuggestionList to stack suggestions in a column.
"use client";
import {
Suggestions,
SuggestionList,
Suggestion,
} from "@/components/nexus-ui/suggestions";
export default function SuggestionVertical() {
return (
<Suggestions onSelect={(value) => console.log(value)}>
<SuggestionList orientation="vertical">
<Suggestion>What is AI?</Suggestion>
<Suggestion>Teach me Engineering from scratch</Suggestion>
<Suggestion>Design a weekly workout plan</Suggestion>
</SuggestionList>
</Suggestions>
);
}
With Custom Value
Use the value prop when the display text differs from the value passed to onSelect.
"use client";
import {
Suggestions,
SuggestionList,
Suggestion,
} from "@/components/nexus-ui/suggestions";
export default function SuggestionCustomValue() {
return (
<Suggestions onSelect={(value) => console.log(value)}>
<SuggestionList>
<Suggestion value="Explain artificial intelligence in simple terms">
What is AI?
</Suggestion>
<Suggestion value="Create a beginner-friendly React tutorial">
How to learn React?
</Suggestion>
</SuggestionList>
</Suggestions>
);
}
With Icons
Since Suggestion renders a shadcn Button, you can add icons alongside text.
"use client";
import {
Suggestions,
SuggestionList,
Suggestion,
} from "@/components/nexus-ui/suggestions";
import {
AiMagicIcon,
CodeSimpleIcon,
Dumbbell02Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
export default function SuggestionWithIcons() {
return (
<Suggestions onSelect={(value) => console.log(value)}>
<SuggestionList>
<Suggestion className="gap-1.5">
<HugeiconsIcon icon={AiMagicIcon} strokeWidth={2.0} className="size-3.5" />
What is AI?
</Suggestion>
<Suggestion className="gap-1.5">
<HugeiconsIcon icon={CodeSimpleIcon} strokeWidth={2.0} className="size-3.5" />
How to learn React?
</Suggestion>
<Suggestion className="gap-1.5">
<HugeiconsIcon icon={Dumbbell02Icon} strokeWidth={2.0} className="size-3.5" />
Design a workout plan
</Suggestion>
</SuggestionList>
</Suggestions>
);
}
With Prompt Input
Clicking a suggestion populates the PromptInput textarea, combining both components.
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import PromptInput, {
PromptInputActions,
PromptInputAction,
PromptInputActionGroup,
PromptInputTextarea,
} from "@/components/nexus-ui/prompt-input";
import {
Suggestions,
SuggestionList,
Suggestion,
} from "@/components/nexus-ui/suggestions";
import { ArrowUp02Icon, PlusSignIcon, SquareIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
type InputStatus = "idle" | "loading" | "error" | "submitted";
export default function SuggestionWithPromptInput() {
const [input, setInput] = React.useState("");
const [status, setStatus] = React.useState<InputStatus>("idle");
const doSubmit = React.useCallback((value: string) => {
const trimmed = value.trim();
if (!trimmed) return;
setInput("");
setStatus("loading");
setTimeout(() => {
setStatus("submitted");
setTimeout(() => setStatus("idle"), 800);
}, 2500);
}, []);
const isLoading = status === "loading";
return (
<div className="flex w-full flex-col gap-6">
<PromptInput onSubmit={doSubmit}>
<PromptInputTextarea
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isLoading}
/>
<PromptInputActions>
<PromptInputActionGroup>
<PromptInputAction asChild>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="cursor-pointer rounded-full text-secondary-foreground active:scale-97 disabled:opacity-70 hover:dark:bg-secondary"
>
<HugeiconsIcon icon={PlusSignIcon} strokeWidth={2.0} className="size-4" />
</Button>
</PromptInputAction>
</PromptInputActionGroup>
<PromptInputActionGroup>
<PromptInputAction asChild>
<Button
type="button"
size="icon-sm"
className="cursor-pointer rounded-full active:scale-97 disabled:opacity-70"
disabled={!isLoading && !input.trim()}
onClick={() => input.trim() && doSubmit(input)}
>
{isLoading ? (
<HugeiconsIcon icon={SquareIcon} strokeWidth={2.0} className="size-3.5 fill-current" />
) : (
<HugeiconsIcon icon={ArrowUp02Icon} strokeWidth={2.0} className="size-4" />
)}
</Button>
</PromptInputAction>
</PromptInputActionGroup>
</PromptInputActions>
</PromptInput>
<Suggestions onSelect={(value) => setInput(value)}>
<SuggestionList className="justify-center">
<Suggestion>What is AI?</Suggestion>
<Suggestion>Teach me Engineering from scratch</Suggestion>
<Suggestion>How to learn React?</Suggestion>
<Suggestion>Design a weekly workout plan</Suggestion>
<Suggestion>Places to visit in France</Suggestion>
</SuggestionList>
</Suggestions>
</div>
);
}
With Panel
Category chips that open a panel with related suggestions. Uses the highlight prop to style matching terms.
"use client";
import { useState, useRef, useCallback } from "react";
import { Button } from "@/components/ui/button";
import PromptInput, {
PromptInputActions,
PromptInputAction,
PromptInputActionGroup,
PromptInputTextarea,
} from "@/components/nexus-ui/prompt-input";
import {
Suggestions,
SuggestionList,
Suggestion,
SuggestionPanel,
SuggestionPanelHeader,
SuggestionPanelTitle,
SuggestionPanelClose,
SuggestionPanelContent,
} from "@/components/nexus-ui/suggestions";
import {
AiMagicIcon,
ArrowRight01Icon,
ArrowUp02Icon,
BookOpenTextIcon,
Cancel01Icon,
MapsIcon,
PencilEdit01Icon,
PlusSignIcon,
SquareIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { cn } from "@/lib/utils";
function PlanIcon({ className }: { className?: string }) {
return <HugeiconsIcon icon={MapsIcon} strokeWidth={2.0} className={className} />;
}
function ResearchIcon({ className }: { className?: string }) {
return <HugeiconsIcon icon={BookOpenTextIcon} strokeWidth={2.0} className={className} />;
}
function WriteIcon({ className }: { className?: string }) {
return <HugeiconsIcon icon={PencilEdit01Icon} strokeWidth={2.0} className={className} />;
}
function BrainstormIcon({ className }: { className?: string }) {
return <HugeiconsIcon icon={AiMagicIcon} strokeWidth={2.0} className={className} />;
}
const categories = [
{
label: "Plan",
icon: PlanIcon,
highlight: "Make a",
suggestions: [
"Make a plan to save on the downpayment of a house",
"Make a dinner cooking plan for this week on a family of 4",
"Make a HIIT workout plan",
"Make a plan to eat healthier",
],
},
{
label: "Research",
icon: ResearchIcon,
highlight: "Research",
suggestions: [
"Research the best programming languages to learn in 2025",
"Research how to start a small business",
"Research the pros and cons of remote work",
"Research sustainable energy solutions",
],
},
{
label: "Write",
icon: WriteIcon,
highlight: "Write a",
suggestions: [
"Write a cover letter for a software engineer role",
"Write a short story about time travel",
"Write a professional email to follow up after an interview",
"Write a blog post about productivity tips",
],
},
{
label: "Brainstorm",
icon: BrainstormIcon,
highlight: "Brainstorm",
suggestions: [
"Brainstorm side project ideas for a developer portfolio",
"Brainstorm creative date night ideas",
"Brainstorm ways to improve team productivity",
"Brainstorm names for a new startup",
],
},
];
type InputStatus = "idle" | "loading" | "error" | "submitted";
export default function SuggestionWithPanel() {
const [input, setInput] = useState("");
const [status, setStatus] = useState<InputStatus>("idle");
const [open, setOpen] = useState(false);
const [activeCategory, setActiveCategory] = useState<string | null>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const active = categories.find((c) => c.label === activeCategory);
const handleCategoryClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement>, category: string) => {
triggerRef.current = e.currentTarget;
setActiveCategory(activeCategory === category ? null : category);
setOpen(activeCategory === category ? false : true);
},
[activeCategory],
);
const handleOpenChange = useCallback((next: boolean) => {
setOpen(next);
}, []);
const handleClose = useCallback(() => {
triggerRef.current?.focus();
setActiveCategory(null);
}, []);
const doSubmit = useCallback((value: string) => {
const trimmed = value.trim();
if (!trimmed) return;
setInput("");
setStatus("loading");
setTimeout(() => {
setStatus("submitted");
setTimeout(() => setStatus("idle"), 800);
}, 2500);
}, []);
const isLoading = status === "loading";
return (
<div className="flex w-full flex-col gap-6">
<PromptInput onSubmit={doSubmit} className="z-2 shadow-sm">
<PromptInputTextarea
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isLoading}
/>
<PromptInputActions>
<PromptInputActionGroup>
<PromptInputAction asChild>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="cursor-pointer rounded-full text-secondary-foreground active:scale-97 disabled:opacity-70 hover:dark:bg-secondary"
>
<HugeiconsIcon icon={PlusSignIcon} strokeWidth={2.0} className="size-4" />
</Button>
</PromptInputAction>
</PromptInputActionGroup>
<PromptInputActionGroup>
<PromptInputAction asChild>
<Button
type="button"
size="icon-sm"
className="cursor-pointer rounded-full active:scale-97 disabled:opacity-70"
disabled={!isLoading && !input.trim()}
onClick={() => input.trim() && doSubmit(input)}
>
{isLoading ? (
<HugeiconsIcon icon={SquareIcon} strokeWidth={2.0} className="size-3.5 fill-current" />
) : (
<HugeiconsIcon icon={ArrowUp02Icon} strokeWidth={2.0} className="size-4" />
)}
</Button>
</PromptInputAction>
</PromptInputActionGroup>
</PromptInputActions>
</PromptInput>
<div className="relative">
<Suggestions>
<SuggestionList className="justify-center">
{categories.map((category) => (
<Suggestion
key={category.label}
variant="filled"
onClick={(e) => handleCategoryClick(e, category.label)}
className={cn(open && "opacity-0")}
>
<category.icon className="size-3.5" />
{category.label}
</Suggestion>
))}
</SuggestionList>
</Suggestions>
<SuggestionPanel
open={open}
onOpenChange={handleOpenChange}
onClose={handleClose}
>
{active && (
<>
<SuggestionPanelHeader className="h-6">
<SuggestionPanelTitle>
<active.icon className="size-3.5 text-ring" />
<span className="text-[13px] font-normal text-ring">
{active.label}
</span>
</SuggestionPanelTitle>
<SuggestionPanelClose className="-mr-0.75 size-5">
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2.0} className="size-4" />
</SuggestionPanelClose>
</SuggestionPanelHeader>
<SuggestionPanelContent>
<Suggestions
onSelect={(value) => {
setInput(value);
setOpen(false);
}}
>
<SuggestionList orientation="vertical" className="gap-2">
{active.suggestions.map((text) => (
<Suggestion
key={text}
variant="ghost"
highlight={active.highlight}
value={text}
className="group h-auto w-full justify-between rounded-[6px] px-3 text-left whitespace-normal text-primary hover:bg-border"
>
{text}
<HugeiconsIcon
icon={ArrowRight01Icon}
strokeWidth={2.0}
className="size-4 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
/>
</Suggestion>
))}
</SuggestionList>
</Suggestions>
</SuggestionPanelContent>
</>
)}
</SuggestionPanel>
</div>
</div>
);
}
Vercel AI SDK Integration
Combine Suggestions with Prompt Input and the Vercel AI SDK for a chat interface with quick-start prompts.
Install the AI SDK
npm install ai @ai-sdk/react @ai-sdk/openaiCreate your chat API route
See Prompt Input docs for the route implementation.
Wire Suggestions + Prompt Input to useChat
"use client";
import { useState } from "react";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { Button } from "@/components/ui/button";
import PromptInput, {
PromptInputActions,
PromptInputAction,
PromptInputActionGroup,
PromptInputTextarea,
} from "@/components/nexus-ui/prompt-input";
import {
Suggestions,
SuggestionList,
Suggestion,
} from "@/components/nexus-ui/suggestions";
import {
ArrowUp02Icon,
PlusSignIcon,
SquareIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
export default function ChatWithSuggestions() {
const { sendMessage, status } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
const [input, setInput] = useState("");
const isLoading = status !== "ready";
const handleSubmit = (e?: React.FormEvent) => {
e?.preventDefault();
if (input.trim()) {
sendMessage({ text: input });
setInput("");
}
};
return (
<div className="flex w-full flex-col gap-6">
<form onSubmit={handleSubmit} className="w-full">
<PromptInput onSubmit={handleSubmit}>
<PromptInputTextarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask anything..."
disabled={isLoading}
/>
<PromptInputActions>
<PromptInputActionGroup>
<PromptInputAction asChild>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="cursor-pointer rounded-full text-secondary-foreground active:scale-97 disabled:opacity-70 hover:dark:bg-secondary"
>
<HugeiconsIcon icon={PlusSignIcon} strokeWidth={2.0} className="size-4" />
</Button>
</PromptInputAction>
</PromptInputActionGroup>
<PromptInputActionGroup>
<PromptInputAction asChild>
<Button
type="submit"
size="icon-sm"
className="cursor-pointer rounded-full active:scale-97 disabled:opacity-70"
disabled={isLoading || !input.trim()}
>
{isLoading ? (
<HugeiconsIcon icon={SquareIcon} strokeWidth={2.0} className="size-3.5 fill-current" />
) : (
<HugeiconsIcon icon={ArrowUp02Icon} strokeWidth={2.0} className="size-4" />
)}
</Button>
</PromptInputAction>
</PromptInputActionGroup>
</PromptInputActions>
</PromptInput>
</form>
<Suggestions onSelect={(value) => setInput(value)}>
<SuggestionList className="justify-center">
<Suggestion>What is AI?</Suggestion>
<Suggestion>Teach me Engineering from scratch</Suggestion>
<Suggestion>How to learn React?</Suggestion>
<Suggestion>Design a weekly workout plan</Suggestion>
<Suggestion>Places to visit in France</Suggestion>
</SuggestionList>
</Suggestions>
</div>
);
}For one-click submission (suggestion sends immediately without editing), call sendMessage in onSelect:
<Suggestions
onSelect={(value) => {
if (value.trim()) {
sendMessage({ text: value });
setInput("");
}
}}
>API Reference
Suggestions
The root container that provides onSelect context to all child Suggestion components. Extends React.HTMLAttributes<HTMLDivElement>.
Prop
Type
SuggestionList
Layout wrapper for arranging suggestions horizontally or vertically. Extends React.HTMLAttributes<HTMLDivElement>.
Prop
Type
Suggestion
A clickable pill that triggers onSelect from the parent Suggestions context. Renders as a shadcn Button. Extends Button props (except variant).
Prop
Type
SuggestionPanel
Full-width panel that displays below the input, covering the category pills. Uses Presence for enter/exit animations. Closes on Escape via onOpenChange. Use with SuggestionPanelHeader, SuggestionPanelTitle, SuggestionPanelClose, and SuggestionPanelContent.
Prop
Type
SuggestionPanelHeader
Header row for the panel. Typically contains SuggestionPanelTitle and SuggestionPanelClose. Extends React.HTMLAttributes<HTMLDivElement>.
Prop
Type
SuggestionPanelTitle
Title area for the panel header. Use for the category icon and label. Extends React.HTMLAttributes<HTMLDivElement>.
Prop
Type
SuggestionPanelClose
Close button for the panel. Clicking it calls onOpenChange(false) via context. Use asChild to merge props onto a child element. Extends React.ButtonHTMLAttributes<HTMLButtonElement>.
Prop
Type
SuggestionPanelContent
Content wrapper for the panel's suggestion list. Use with Suggestions and SuggestionList inside. Use asChild to merge props onto a child element. Extends React.HTMLAttributes<HTMLDivElement>.
Prop
Type