Suggestions

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/suggestions
pnpm dlx shadcn@latest add @nexus-ui/suggestions
yarn dlx shadcn@latest add @nexus-ui/suggestions
bunx shadcn@latest add @nexus-ui/suggestions

Copy and paste the following code into your project.

components/nexus-ui/suggestions.tsx
"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/openai

Create 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