Documentation

AI Elements

shadcn/ui components for AI SDK chat UIs. Messages, prompts, tools, reasoning, and artifacts.

AI Elements are React components for AI SDK chat UIs: messages, prompts, tool calls, reasoning, sources, and artifacts. Patterns that use them ship the files under @/components/ai-elements.

Peer dependency:

pnpm add ai @ai-sdk/react

Components

Message

One message in a conversation.

import {
  Message,
  MessageAvatar,
  MessageContent,
} from "@/components/ai-elements/message";
 
export function ChatMessage({ message }) {
  return (
    <Message from={message.role}>
      <MessageAvatar src={message.avatar} name={message.name} />
      <MessageContent variant="contained">
        {message.parts.map((part, index) =>
          part.type === "text" ? <span key={index}>{part.text}</span> : null
        )}
      </MessageContent>
    </Message>
  );
}

Props:

  • from: "user" | "assistant" - The role of the message sender
  • variant: "contained" | "flat" - Visual style variant

MessageContent

Message body. Styles differ for user and assistant roles.

<MessageContent variant="contained">
  <p>Your message content here</p>
</MessageContent>

MessageAvatar

Avatar for the sender.

<MessageAvatar src="/path/to/avatar.jpg" name="John Doe" />

Conversation

Chat scroll container with auto-scroll.

import {
  Conversation,
  ConversationContent,
} from "@/components/ai-elements/conversation";
 
export function ChatInterface() {
  return (
    <Conversation>
      <ConversationContent>{/* Your messages here */}</ConversationContent>
    </Conversation>
  );
}

ConversationEmptyState

Empty chat placeholder.

import { ConversationEmptyState } from "@/components/ai-elements/conversation";
 
<ConversationEmptyState
  title="No messages yet"
  description="Start a conversation to see messages here"
  icon={<ChatIcon />}
/>;

ConversationScrollButton

Jump back to the latest message after scrolling up.

import { ConversationScrollButton } from "@/components/ai-elements/conversation";
 
<ConversationScrollButton />;

PromptInput

Composer for chat: textarea, attachments, and submit.

import {
  PromptInput,
  PromptInputAttachment,
  PromptInputAttachments,
  PromptInputBody,
  PromptInputFooter,
  PromptInputSubmit,
  PromptInputTextarea,
} from "@/components/ai-elements/prompt-input";
 
export function ChatInput({ onSubmit }) {
  return (
    <PromptInput onSubmit={onSubmit}>
      <PromptInputBody>
        <PromptInputAttachments>
          {(attachment) => <PromptInputAttachment data={attachment} />}
        </PromptInputAttachments>
        <PromptInputTextarea placeholder="What would you like to know?" />
      </PromptInputBody>
      <PromptInputFooter>
        <PromptInputSubmit />
      </PromptInputFooter>
    </PromptInput>
  );
}

Features:

  • File drag & drop support
  • Image preview
  • Auto-resizing textarea
  • Keyboard shortcuts (Enter to send, Shift+Enter for new line)
  • Paste file support

PromptInputSubmit

Submit control. Icon follows status.

<PromptInputSubmit status="streaming" />

Status options:

  • "submitted" - Shows loading spinner
  • "streaming" - Shows stop icon
  • "error" - Shows error icon
  • undefined - Shows send icon

Response

Renders streamed assistant text.

import { Response } from "@/components/ai-elements/response";
 
export function AIResponse({ content }) {
  return <Response>{content}</Response>;
}

Loader

Spinner while a reply is pending.

import { Loader } from "@/components/ai-elements/loader";
 
<Loader size={16} />;

Suggestions

Horizontal list of suggested prompts.

import { Suggestion, Suggestions } from "@/components/ai-elements/suggestion";
 
export function SuggestionList({ suggestions, onSuggestionClick }) {
  return (
    <Suggestions>
      {suggestions.map((suggestion) => (
        <Suggestion
          key={suggestion}
          suggestion={suggestion}
          onClick={onSuggestionClick}
        />
      ))}
    </Suggestions>
  );
}

Tool

Collapsible panel for one tool call (input, output, error).

import {
  Tool,
  ToolContent,
  ToolHeader,
  ToolInput,
  ToolOutput,
} from "@/components/ai-elements/tool";
 
export function ToolExecution({ tool }) {
  return (
    <Tool>
      <ToolHeader title={tool.name} type={tool.type} state={tool.state} />
      <ToolContent>
        <ToolInput input={tool.input} />
        <ToolOutput output={tool.output} errorText={tool.errorText} />
      </ToolContent>
    </Tool>
  );
}

Tool States:

  • "input-streaming" - Tool is receiving input
  • "input-available" - Tool is processing
  • "output-available" - Tool completed successfully
  • "output-error" - Tool encountered an error

GeneratedImage

Shows a model-generated image from base64 data.

import { GeneratedImage } from "@/components/ai-elements/image";
 
<GeneratedImage
  base64={imageData.base64}
  mediaType={imageData.mediaType}
  alt="AI generated image"
/>;

Actions

Row of message actions (retry, copy, and similar).

import { Action, Actions } from "@/components/ai-elements/actions";
 
export function MessageActions() {
  return (
    <Actions>
      <Action tooltip="Retry message" label="Retry">
        <RetryIcon />
      </Action>
      <Action tooltip="Copy message" label="Copy">
        <CopyIcon />
      </Action>
    </Actions>
  );
}

Props:

  • tooltip: Optional tooltip text
  • label: Accessibility label
  • variant: Button variant
  • size: Button size

Context

Token usage and estimated cost for the current model.

import {
  Context,
  ContextContent,
  ContextContentBody,
  ContextContentFooter,
  ContextContentHeader,
  ContextInputUsage,
  ContextOutputUsage,
  ContextTrigger,
} from "@/components/ai-elements/context";
 
export function ContextUsage({ usage, modelId }) {
  return (
    <Context
      usedTokens={usage.inputTokens + usage.outputTokens}
      maxTokens={8000}
      usage={usage}
      modelId={modelId}
    >
      <ContextTrigger />
      <ContextContent>
        <ContextContentHeader />
        <ContextContentBody>
          <ContextInputUsage />
          <ContextOutputUsage />
        </ContextContentBody>
        <ContextContentFooter />
      </ContextContent>
    </Context>
  );
}

Features:

  • Token usage visualization
  • Cost estimation
  • Progress indicators
  • Hover card display

Artifact

Shell for generated artifacts (tables, charts, documents).

import {
  Artifact,
  ArtifactAction,
  ArtifactActions,
  ArtifactClose,
  ArtifactContent,
  ArtifactDescription,
  ArtifactHeader,
  ArtifactTitle,
} from "@/components/ai-elements/artifact";
 
export function ArtifactDisplay({ title, description, children }) {
  return (
    <Artifact>
      <ArtifactHeader>
        <div>
          <ArtifactTitle>{title}</ArtifactTitle>
          <ArtifactDescription>{description}</ArtifactDescription>
        </div>
        <ArtifactActions>
          <ArtifactAction tooltip="Download" icon={DownloadIcon} />
          <ArtifactAction tooltip="Share" icon={ShareIcon} />
          <ArtifactClose />
        </ArtifactActions>
      </ArtifactHeader>
      <ArtifactContent>{children}</ArtifactContent>
    </Artifact>
  );
}

Sources

Citation list for retrieved or searched sources.

import {
  Source,
  Sources,
  SourcesContent,
  SourcesTrigger,
} from "@/components/ai-elements/sources";
 
export function SourceCitations({ sources }) {
  return (
    <Sources>
      <SourcesTrigger count={sources.length} />
      <SourcesContent>
        {sources.map((source, index) => (
          <Source key={index} href={source.url} title={source.title} />
        ))}
      </SourcesContent>
    </Sources>
  );
}

Branch

Switch between alternate assistant replies.

import {
  Branch,
  BranchMessages,
  BranchNext,
  BranchPage,
  BranchPrevious,
  BranchSelector,
} from "@/components/ai-elements/branch";
 
export function BranchingChat({ messages }) {
  return (
    <Branch defaultBranch={0}>
      <BranchMessages>
        {messages.map((branch, index) => (
          <div key={index}>
            {branch.map((message) => (
              <Message key={message.id} from={message.role}>
                <MessageContent>
                  {message.parts.map((part, partIndex) =>
                    part.type === "text" ? (
                      <span key={partIndex}>{part.text}</span>
                    ) : null
                  )}
                </MessageContent>
              </Message>
            ))}
          </div>
        ))}
      </BranchMessages>
      <BranchSelector from="assistant">
        <BranchPrevious />
        <BranchPage />
        <BranchNext />
      </BranchSelector>
    </Branch>
  );
}

Features:

  • Navigate between different conversation branches
  • Automatic branch detection
  • Keyboard navigation support

ChainOfThought

Step-by-step reasoning UI.

import {
  ChainOfThought,
  ChainOfThoughtContent,
  ChainOfThoughtHeader,
  ChainOfThoughtImage,
  ChainOfThoughtSearchResult,
  ChainOfThoughtSearchResults,
  ChainOfThoughtStep,
} from "@/components/ai-elements/chain-of-thought";
 
export function ReasoningDisplay({ steps }) {
  return (
    <ChainOfThought>
      <ChainOfThoughtHeader>Analysis Process</ChainOfThoughtHeader>
      <ChainOfThoughtContent>
        {steps.map((step, index) => (
          <ChainOfThoughtStep
            key={index}
            label={step.label}
            description={step.description}
            status={step.status}
            icon={step.icon}
          >
            {step.content}
          </ChainOfThoughtStep>
        ))}
        <ChainOfThoughtSearchResults>
          <ChainOfThoughtSearchResult>
            Search Result 1
          </ChainOfThoughtSearchResult>
          <ChainOfThoughtSearchResult>
            Search Result 2
          </ChainOfThoughtSearchResult>
        </ChainOfThoughtSearchResults>
        <ChainOfThoughtImage caption="Analysis visualization">
          <img src="/analysis-chart.png" alt="Analysis" />
        </ChainOfThoughtImage>
      </ChainOfThoughtContent>
    </ChainOfThought>
  );
}

Reasoning

Collapsible thinking block. Opens while streaming, then can collapse.

import {
  Reasoning,
  ReasoningContent,
  ReasoningTrigger,
} from "@/components/ai-elements/reasoning";
 
export function AIReasoning({ isStreaming, reasoning }) {
  return (
    <Reasoning isStreaming={isStreaming} defaultOpen={true}>
      <ReasoningTrigger />
      <ReasoningContent>{reasoning}</ReasoningContent>
    </Reasoning>
  );
}

Features:

  • Auto-opens during streaming
  • Auto-closes after completion
  • Duration tracking
  • Collapsible interface

Example

Chat shell with useChat:

"use client";
 
import { useChat } from "@ai-sdk/react";
 
import {
  Conversation,
  ConversationContent,
  ConversationEmptyState,
  ConversationScrollButton,
  Loader,
  Message,
  MessageAvatar,
  MessageContent,
  PromptInput,
  PromptInputBody,
  PromptInputFooter,
  PromptInputSubmit,
  PromptInputTextarea,
  Response,
} from "@/components/ai-elements";
 
export function ChatInterface() {
  const { messages, sendMessage, status } = useChat();
 
  return (
    <div className="flex h-screen flex-col">
      <Conversation>
        <ConversationContent>
          {messages.length === 0 ? (
            <ConversationEmptyState
              title="Start a conversation"
              description="Ask me anything!"
            />
          ) : (
            messages.map((message) => (
              <Message key={message.id} from={message.role}>
                <MessageAvatar
                  src={
                    message.role === "user"
                      ? "/user-avatar.jpg"
                      : "/ai-avatar.jpg"
                  }
                  name={message.role === "user" ? "You" : "AI"}
                />
                <MessageContent>
                  {message.parts.map((part, index) =>
                    part.type === "text" ? (
                      <Response key={index}>{part.text}</Response>
                    ) : null
                  )}
                </MessageContent>
              </Message>
            ))
          )}
          {status === "submitted" && (
            <Message from="assistant">
              <MessageAvatar src="/ai-avatar.jpg" name="AI" />
              <MessageContent>
                <Loader />
              </MessageContent>
            </Message>
          )}
        </ConversationContent>
        <ConversationScrollButton />
      </Conversation>
 
      <PromptInput
        onSubmit={(message) => {
          if (message.text.trim()) {
            sendMessage({ text: message.text });
          }
        }}
      >
        <PromptInputBody>
          <PromptInputTextarea placeholder="What would you like to know?" />
        </PromptInputBody>
        <PromptInputFooter>
          <PromptInputSubmit status={status} />
        </PromptInputFooter>
      </PromptInput>
    </div>
  );
}

Point useChat at your route with a transport when you are not using the default /api/chat:

"use client";
 
import { DefaultChatTransport } from "ai";
import { useChat } from "@ai-sdk/react";
 
const { messages, sendMessage, status } = useChat({
  transport: new DefaultChatTransport({ api: "/api/chat" }),
});

Theming

AI Elements use the same CSS variables as shadcn/ui. See Theming.

Next