AI chat components for React and shadcn/ui

32 free React components for the parts of an AI chat that a basic chat kit leaves plain: the prompt box, the wait before the first word, tool calls and approvals, the streamed answer, citations, and what happens when something fails or runs out. You install each with the shadcn CLI. They take your theme, use no animation library and need no provider or runtime, and 18 of them come with a recipe for the Vercel AI SDK’s useChat.

components for AI chat
32
stages of a conversation covered
4
with an AI SDK recipe
18
npm package added (lucide-react)
1

Updated October 16, 2026. Counts on this page are read from the components’ own documentation.

01Together

See them working together

A scripted conversation, played through the real components: the prompt types itself, the agent thinks, searches, calls a tool and streams its answer.

This is the Agent playback component. Nothing here is a recording: it pauses off-screen and shows the finished conversation, still, with reduced motion.

02The parts

What a chat needs, in the order it happens

Each links to its page with a live demo, its states and the code. 28 of the 32 also install Number roll or Text morph, the two small components behind their rolling numbers and morphing labels.

Asking7

Where the person writes, attaches things and chooses how the model should work.

  • Prompt composer

    The message box: grows as you type, takes files by paperclip, paste or drop, and turns Send into Stop while it answers.

  • Mentions and commands

    @ for files and people, / for commands: a menu at the caret that turns the pick into a chip in the text.

  • Attachment chip

    One file’s journey into the prompt: uploading, being read, ready or failed.

  • Mode switcher

    Fast, Thinking or Research: a segmented switch that says what each mode does, with locked paid modes.

  • Model picker

    Choose the model and the thinking effort from a pill that grows into a menu.

  • Voice input

    Dictate a prompt, with a live waveform from the microphone and a transcribing state.

  • Agent composer

    The taller box for coding and operations agents: branch, folder, context ring, permissions and model.

    Also installs Model picker.

While it works10

The wait before the first word, and everything an agent does before it answers.

  • Thinking indicator

    The gap before the first token: four loaders with a label and a timer that ends in “Done in 12.4s”.

  • Thinking trace

    An expandable record of steps, reasoning, search results or tool calls that folds into “Thought for 6s”.

  • Web research

    The search as it happens, then “3 searches · 9 sources”, with every source a link.

  • Tool call

    One tool call from arguments to result: preparing, running, done, failed or denied.

  • Clarifying question

    The agent asks before it guesses: options with number keys, a free-text row, then a one-line receipt.

  • Approval card

    Asks before an action that can’t be undone, then becomes a one-line record: approved, denied or expired.

  • Plan

    The agent’s checklist, ticking itself off, with a failed state that stays open.

  • Command output

    A command the agent runs: the log streams, folds away on success and stays open on failure.

  • Task progress

    Work that takes minutes: phases, a live timer, stats, Cancel, then the result.

  • Task log

    What the agent did, task by task, as steps on a trail with file chips.

The answer and after8

Showing the answer as it arrives, and what people do with it.

  • Streaming answer

    The answer as it arrives, with citations in the sentence, Copy and feedback actions, sources and follow-up questions.

  • Citation

    A numbered source in the sentence that previews the site and snippet on hover or focus.

  • Code block

    Code that highlights while it streams, with Copy, line wrap, folding for long files and Apply.

    Also installs Status button.

  • Response versions

    Step between regenerated answers with “2 / 3”, without losing the old ones.

  • Selection actions

    Select text and a pill offers actions; the rewrite streams into the selection with Keep and Discard.

  • Inline rewrite

    A dial that rewrites a paragraph shorter, longer or in another tone, in place.

  • Diff review

    Accept or reject what the AI changed, word by word or line by line.

  • Action receipts

    What the agent did, one line each, with a timed Undo.

Around the conversation6

Scrolling, history, errors and limits: the parts that decide whether people trust it.

  • Jump to latest

    Follows the answer as it streams, lets go the moment the reader scrolls up, and offers “Jump to latest”.

  • Chat history

    Past chats grouped by day, searchable, with rename, pin and delete with Undo.

  • Edit and resend

    Edit a sent message in place and branch the conversation, with “2 / 3” to step between versions.

  • Error and limit states

    Failed answers, offline, rate limits with a live countdown and chats that grow too long.

    Also installs Status button.

  • Usage meter

    Credits, messages or context left: quiet until it matters.

  • Usage limits

    The context window split by what fills it, and each plan limit with its reset time.

For your landing page1

Show the product working before anyone signs up.

  • Agent playback

    A scripted conversation played through the real components above, for heroes and demos.

    Also installs Prompt composer, Thinking trace, Web research, Tool call, Streaming answer.

Install any of them, for example the prompt box

$ npx shadcn@latest add https://hairlineui.com/r/prompt-composer.json

03Wire it up

A chat with the AI SDK

Six of the components in one client component. The server is any streamText route.

chat.tsx
"use client";import { useChat } from "@ai-sdk/react";import { lastAssistantMessageIsCompleteWithApprovalResponses, type UIMessage } from "ai";import { ChatScroll } from "@/components/chat-scroll";import { PromptComposer } from "@/components/prompt-composer";import { StreamingText, AnswerActions } from "@/components/streaming-answer";import { ToolCall, type ToolCallStatus } from "@/components/tool-call";import { ApprovalCard } from "@/components/approval-card";import { ChatNotice } from "@/components/chat-notice";type ToolPart = Extract<UIMessage["parts"][number], { toolCallId: string }>;function toolStatus(part: ToolPart): ToolCallStatus {  switch (part.state) {    case "input-streaming": return "preparing";    case "input-available": return "running";    case "output-available": return "done";    case "output-error": return "error";    case "approval-responded": return part.approval.approved ? "running" : "denied";    default: return "denied"; // output-denied  }}export function Chat() {  const { messages, sendMessage, status, stop, error, clearError, regenerate, addToolApprovalResponse } = useChat({    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,  });  const lastUser = messages.findLast((m) => m.role === "user");  return (    <div className="flex h-dvh flex-col">      <ChatScroll        streaming={status === "streaming"}        followKey={lastUser?.id} // sending a message brings the reader back down        className="flex-1"        contentClassName="mx-auto flex max-w-2xl flex-col gap-6 px-4 py-6"      >        {messages.map((message, index) => {          const text = message.parts.map((p) => (p.type === "text" ? p.text : "")).join("");          if (message.role === "user") return <p key={message.id}>{text}</p>;          const live = status === "streaming" && index === messages.length - 1;          return (            <div key={message.id} className="flex flex-col gap-3">              {message.parts.map((part) => {                if (!("toolCallId" in part)) return null;                const name = part.type.replace(/^tool-/, "");                if (part.state === "approval-requested") {                  return (                    <ApprovalCard                      key={part.toolCallId}                      title={`Run ${name}`}                      reason={part.approval.requestReason}                      onApprove={() => addToolApprovalResponse({ id: part.approval.id, approved: true })}                      onDeny={() => addToolApprovalResponse({ id: part.approval.id, approved: false })}                    />                  );                }                return (                  <ToolCall                    key={part.toolCallId}                    name={name}                    status={toolStatus(part)}                    input={part.input}                    output={part.state === "output-available" ? part.output : undefined}                    errorText={part.state === "output-error" ? part.errorText : undefined}                  />                );              })}              <StreamingText content={[text]} status={live ? "streaming" : "done"} />              <AnswerActions disabled={live} copyText={text} onRegenerate={() => regenerate({ messageId: message.id })} />            </div>          );        })}        {status === "error" && (          <ChatNotice kind="error" details={error?.message} onRetry={() => { clearError(); regenerate(); }} />        )}      </ChatScroll>      <PromptComposer status={status} onSubmit={(text) => sendMessage({ text })} onStop={stop} />    </div>  );}
  • Status passes straight through. status from useChat goes to the prompt box, which shows Send, a spinner while waiting for the first token, then Stop while the answer streams.
  • Tool parts map to states. The AI SDK’s tool states become the card’s: preparing, running, done, failed or denied. A call that waits for approval is shown as an approval card instead.
  • Approvals need two lines on the server and one option on the client. On the server, toolApproval: { sendEmail: "user-approval" } in streamText. On the client, sendAutomaticallyWhen continues the conversation once every approval is answered.
  • Check the AI SDK docs for your version. The tool-part states and approval option are from the current AI SDK documentation; they have changed between major versions.

04Details

Details most AI chat UIs get wrong

Small things that make a chat feel unreliable, and how each component handles them.

  • In many chat UIs

    The chat pulls the reader back down while they are reading

    How it’s handled · Jump to latest

    It follows the answer only while the reader is at the bottom, and lets go on the first upward wheel, touch drag, Page Up, Up arrow or Home, before the next line of text can pull them back. A “Jump to latest” button waits, and says “Writing” while text arrives. It tells the reader’s scrolling from the page resizing itself: a scroll only counts if they recently used the wheel, touch, keys or scrollbar. See Jump to latest →

  • In many chat UIs

    Every new word redraws the whole answer

    How it’s handled · Streaming answer

    Words keep a stable key, so only the newly arrived ones mount and fade in, and finished paragraphs are memoised so a long answer stays smooth while the last one grows. See Streaming answer →

  • In many chat UIs

    A stray Enter approves an action

    How it’s handled · Approval card

    Approve is never focused automatically, and the details of what is being approved wrap instead of truncating, because they are what the person is agreeing to. See Approval card →

  • In many chat UIs

    Enter sends the message while an IME is still composing

    How it’s handled · Prompt composer

    Enter sends only when the person is not composing with an IME, so Chinese, Japanese and Korean input works. See Prompt composer →

  • In many chat UIs

    A countdown that screen readers read out every second

    How it’s handled · Error and limit states

    The ticking digits are hidden from assistive technology and the notice announces the time it resets instead, so the countdown never chatters. See Error and limit states →

  • In many chat UIs

    A failed answer is a red box that says “Error”

    How it’s handled · Error and limit states

    It says what happened, that the message is safe, and offers the one action that helps: Retry, which shows Retrying while it runs. Technical details sit behind “Show details”. See Error and limit states →

  • In many chat UIs

    Tool calls dump raw JSON into the conversation

    How it’s handled · Tool call

    Arguments read as fields, nested objects show as compact JSON, long values wrap, and a failed call opens itself once with the reason and a Retry. See Tool call →

  • In many chat UIs

    Regenerating replaces the answer you liked

    How it’s handled · Response versions

    Every version is kept. A pager steps between them with “2 / 3”, and Regenerate shows a spinner while the new one is written. See Response versions →

05Choosing

Hairline, AI Elements or your own

Vercel’s AI Elements and these are both shadcn registries, so they can live in the same project.

Use Vercel’s AI Elements if you want Vercel’s own components, built on shadcn/ui for the AI SDK, with pieces such as a conversation, message, prompt input, reasoning and tool. Check its documentation for the current list.

Use these if you want plain-prop components that take strings and statuses, so they work with any backend, and you care about the states between the happy path and the error: the wait, the approval, the stop, the rate limit, the retry. They add only the lucide-react package.

Build your own if the chat is your product and the interaction is the differentiator. The source of every component is in the install, so you can read or fork how each state is handled.

06Quality

Motion and accessibility, counted

Read from each component’s notes, not claimed in general.

  • Reduced motion documented30 of 32

    What each does under prefers-reduced-motion is written on its page.

  • AI SDK recipe18 of 32

    A working useChat mapping, not just a prop table.

  • npm packages added1

    Only lucide-react. No animation library.

  • Live regionsoutside buttons

    Announcements sit outside the control, and ticking timers are never read out second by second.

07Questions

Before you install

Which parts of an AI chat need their own components?

The prompt box, the wait before the first word, tool calls and approvals, the streamed answer with its citations and actions, and what happens when something fails or runs out. A basic chat kit gives you a message list and an input; these are the states around them that people notice when they are missing.

Do these work with the Vercel AI SDK?

Yes. 18 of the 32 have a recipe for useChat from @ai-sdk/react, and the worked example on this page wires six of them together. The components take plain props and statuses, never SDK message objects, and never call a model themselves, so they also work with any other SDK or your own backend.

Are they free for commercial use?

Yes. Every component in the library is free for personal and commercial projects, with its source, states and motion included. No account is needed to install them.

What does my project need?

React 19, Tailwind CSS v4 and shadcn/ui's theme variables (any style, light and dark). The only npm package any of these 32 adds is lucide-react. There is no animation library, no provider and no runtime to adopt.

Do they respect reduced motion and work with screen readers?

30 of the 32 say in their notes what they do under prefers-reduced-motion, and the component pages list their roles, keyboard keys and what is announced. Announcements sit outside the buttons, and ticking timers are not read out second by second.

How do I install one?

Run npx shadcn@latest add followed by the component’s registry URL, for example https://hairlineui.com/r/prompt-composer.json. Its supporting files and any components it builds on, such as Number roll and Text morph, install with it.