
Ai Elements
- 195 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
Ship polished AI chat UIs with streaming messages, tool-call renders, citations, and loading states using reusable frontend primitives.
About
Provides patterns and components for building modern AI application frontends: conversational layouts, streamed assistant output, structured tool results, and interaction states that match production agent UX expectations.
- Streaming message UI
- Tool-call visualization
- Citation and source blocks
- Prompt input patterns
- Accessible chat layouts
Ai Elements by the numbers
- 195 all-time installs (skills.sh)
- Ranked #2,920 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill ai-elementsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 195 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
What it does
Ship polished AI chat UIs with streaming messages, tool-call renders, citations, and loading states using reusable frontend primitives.
Files
AI Elements
AI Elements is a comprehensive React component library for building AI-powered user interfaces. The library provides 30+ components specifically designed for chat interfaces, tool execution visualization, reasoning displays, and workflow management.
Installation
Install via shadcn registry:
npx shadcn@latest add https://ai-elements.vercel.app/r/[component-name]Import Pattern: Components are imported from individual files, not a barrel export:
// Correct - import from specific files
import { Conversation } from "@/components/ai-elements/conversation";
import { Message } from "@/components/ai-elements/message";
import { PromptInput } from "@/components/ai-elements/prompt-input";
// Incorrect - no barrel export
import { Conversation, Message } from "@/components/ai-elements";Gates (before relying on examples)
Use this sequence when adding or wiring AI Elements so setup is checkable, not assumed.
1. Install each component — Run npx shadcn@latest add https://ai-elements.vercel.app/r/[component-name] for every component you need.
- Pass: The command completes successfully and a file for that component exists under your project’s
components/ai-elements/(or the directorycomponents.jsonuses for those additions).
2. Align import paths — Every import … from "@/components/ai-elements/..." must match your repo’s actual alias and folder layout.
- Pass: Each import resolves to a file on disk (IDE navigation or build/
tscshows no “cannot find module” for those paths).
3. Match `Tool` / `Confirmation` states to your AI SDK — State strings (including approval-related states) depend on the installed ai package major/version.
- Pass: The states you pass to
ToolHeader,Tool, orConfirmationare listed in the AI SDK version you have installed (docs or exported types), not copied from memory alone.
Component Categories
Conversation Components
Components for displaying chat-style interfaces with messages, attachments, and auto-scrolling behavior.
- Conversation: Container with auto-scroll capabilities
- Message: Individual message display with role-based styling
- MessageAttachment: File and image attachments
- MessageBranch: Alternative response navigation
See references/conversation.md for details.
Prompt Input Components
Advanced text input with file attachments, drag-and-drop, speech input, and state management.
- PromptInput: Form container with file handling
- PromptInputTextarea: Auto-expanding textarea
- PromptInputSubmit: Status-aware submit button
- PromptInputAttachments: File attachment display
- PromptInputProvider: Global state management
See references/prompt-input.md for details.
Workflow Components
Components for displaying job queues, tool execution, and approval workflows.
- Queue: Job queue container
- QueueItem: Individual queue items with status
- Tool: Tool execution display with collapsible states
- Confirmation: Approval workflow component
- Reasoning: Collapsible thinking/reasoning display
See references/workflow.md for details.
Visualization Components
ReactFlow-based components for workflow visualization and custom node types.
- Canvas: ReactFlow wrapper with aviation-specific defaults
- Node: Custom node component with handles
- Edge: Temporary and Animated edge types
- Controls, Panel, Toolbar: Navigation and control elements
See references/visualization.md for details.
Integration with shadcn/ui
AI Elements is built on top of shadcn/ui and integrates seamlessly with its theming system:
- Uses shadcn/ui's design tokens (colors, spacing, typography)
- Respects light/dark mode via CSS variables
- Compatible with shadcn/ui components (Button, Card, Collapsible, etc.)
- Follows shadcn/ui's component composition patterns
Key Design Patterns
Component Composition
AI Elements follows a composition-first approach where larger components are built from smaller primitives:
<Tool>
<ToolHeader title="search" type="tool-call-search" state="output-available" />
<ToolContent>
<ToolInput input={{ query: "AI tools" }} />
<ToolOutput output={results} errorText={undefined} />
</ToolContent>
</Tool>Context-Based State
Many components use React Context for state management:
PromptInputProviderfor global input stateMessageBranchfor alternative response navigationConfirmationfor approval workflow stateReasoningfor collapsible thinking state
Controlled vs Uncontrolled
Components support both controlled and uncontrolled patterns:
// Uncontrolled (self-managed state)
<PromptInput onSubmit={handleSubmit} />
// Controlled (external state)
<PromptInputProvider initialInput="">
<PromptInput onSubmit={handleSubmit} />
</PromptInputProvider>Tool State Machine
The Tool component follows the Vercel AI SDK's state machine:
1. input-streaming: Parameters being received 2. input-available: Ready to execute 3. approval-requested: Awaiting user approval (SDK v6) 4. approval-responded: User responded (SDK v6) 5. output-available: Execution completed 6. output-error: Execution failed 7. output-denied: Approval denied
Queue Patterns
Queue components support hierarchical organization:
<Queue>
<QueueSection defaultOpen={true}>
<QueueSectionTrigger>
<QueueSectionLabel count={3} label="tasks" icon={<Icon />} />
</QueueSectionTrigger>
<QueueSectionContent>
<QueueList>
<QueueItem>
<QueueItemIndicator completed={false} />
<QueueItemContent>Task description</QueueItemContent>
</QueueItem>
</QueueList>
</QueueSectionContent>
</QueueSection>
</Queue>Auto-Scroll Behavior
The Conversation component uses the use-stick-to-bottom hook for intelligent auto-scrolling:
- Automatically scrolls to bottom when new messages arrive
- Pauses auto-scroll when user scrolls up
- Provides scroll-to-bottom button when not at bottom
- Supports smooth and instant scroll modes
File Attachment Handling
PromptInput provides comprehensive file handling:
- Drag-and-drop support (local or global)
- Paste image/file support
- File type validation (accept prop)
- File size limits (maxFileSize prop)
- Maximum file count (maxFiles prop)
- Preview for images, icons for files
- Automatic blob URL to data URL conversion on submit
Speech Input
The PromptInputSpeechButton uses the Web Speech API for voice input:
- Browser-based speech recognition
- Continuous recognition mode
- Interim results support
- Automatic text insertion into textarea
- Visual feedback during recording
Reasoning Auto-Collapse
The Reasoning component provides auto-collapse behavior:
- Opens automatically when streaming starts
- Closes 1 second after streaming ends
- Tracks thinking duration in seconds
- Displays "Thinking..." with shimmer effect during streaming
- Shows "Thought for N seconds" when complete
TypeScript Types
All components are fully typed with TypeScript:
import type { ToolUIPart, FileUIPart, UIMessage } from "ai";
type ToolProps = ComponentProps<typeof Collapsible>;
type QueueItemProps = ComponentProps<"li">;
type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
data: FileUIPart;
onRemove?: () => void;
};Common Use Cases
Chat Interface
Combine Conversation, Message, and PromptInput for a complete chat UI:
import { Conversation, ConversationContent, ConversationScrollButton } from "@/components/ai-elements/conversation";
import { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message";
import {
PromptInput,
PromptInputTextarea,
PromptInputFooter,
PromptInputTools,
PromptInputButton,
PromptInputSubmit
} from "@/components/ai-elements/prompt-input";
<div className="flex flex-col h-screen">
<Conversation>
<ConversationContent>
{messages.map(msg => (
<Message key={msg.id} from={msg.role}>
<MessageContent>
<MessageResponse>{msg.content}</MessageResponse>
</MessageContent>
</Message>
))}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
<PromptInput onSubmit={handleSubmit}>
<PromptInputTextarea />
<PromptInputFooter>
<PromptInputTools>
<PromptInputButton onClick={() => attachments.openFileDialog()}>
<PaperclipIcon />
</PromptInputButton>
</PromptInputTools>
<PromptInputSubmit status={chatStatus} />
</PromptInputFooter>
</PromptInput>
</div>Tool Execution Display
Show tool execution with expandable details:
import { Tool, ToolHeader, ToolContent, ToolInput, ToolOutput } from "@/components/ai-elements/tool";
{toolInvocations.map(tool => (
<Tool key={tool.id}>
<ToolHeader
title={tool.toolName}
type={`tool-call-${tool.toolName}`}
state={tool.state}
/>
<ToolContent>
<ToolInput input={tool.args} />
{tool.result && (
<ToolOutput output={tool.result} errorText={tool.error} />
)}
</ToolContent>
</Tool>
))}Approval Workflow
Request user confirmation before executing actions:
import {
Confirmation,
ConfirmationTitle,
ConfirmationRequest,
ConfirmationActions,
ConfirmationAction,
ConfirmationAccepted,
ConfirmationRejected
} from "@/components/ai-elements/confirmation";
<Confirmation approval={tool.approval} state={tool.state}>
<ConfirmationTitle>
Approve deletion of {resource}?
</ConfirmationTitle>
<ConfirmationRequest>
<ConfirmationActions>
<ConfirmationAction onClick={approve} variant="default">
Approve
</ConfirmationAction>
<ConfirmationAction onClick={reject} variant="outline">
Reject
</ConfirmationAction>
</ConfirmationActions>
</ConfirmationRequest>
<ConfirmationAccepted>
Action approved and executed.
</ConfirmationAccepted>
<ConfirmationRejected>
Action rejected.
</ConfirmationRejected>
</Confirmation>Job Queue Management
Display task lists with completion status:
import {
Queue,
QueueSection,
QueueSectionTrigger,
QueueSectionLabel,
QueueSectionContent,
QueueList,
QueueItem,
QueueItemIndicator,
QueueItemContent,
QueueItemDescription
} from "@/components/ai-elements/queue";
<Queue>
<QueueSection>
<QueueSectionTrigger>
<QueueSectionLabel count={todos.length} label="todos" />
</QueueSectionTrigger>
<QueueSectionContent>
<QueueList>
{todos.map(todo => (
<QueueItem key={todo.id}>
<QueueItemIndicator completed={todo.status === 'completed'} />
<QueueItemContent completed={todo.status === 'completed'}>
{todo.title}
</QueueItemContent>
{todo.description && (
<QueueItemDescription completed={todo.status === 'completed'}>
{todo.description}
</QueueItemDescription>
)}
</QueueItem>
))}
</QueueList>
</QueueSectionContent>
</QueueSection>
</Queue>Accessibility
Components include accessibility features:
- ARIA labels and roles
- Keyboard navigation support
- Screen reader announcements
- Focus management
- Semantic HTML elements
Animation
Many components use Framer Motion for smooth animations:
- Shimmer effect for loading states
- Collapsible content transitions
- Edge animations in Canvas
- Loader spinner rotation
References
- Conversation Components
- Prompt Input Components
- Workflow Components
- Visualization Components
Conversation Components
Components for building chat-style interfaces with messages, attachments, and intelligent auto-scrolling.
Core Components
Conversation
Container component that wraps the entire conversation area with auto-scroll functionality.
type ConversationProps = ComponentProps<typeof StickToBottom>;Props:
className?: string- Additional CSS classesinitial?: "smooth" | "auto"- Initial scroll behavior (default: "smooth")resize?: "smooth" | "auto"- Scroll behavior on resize (default: "smooth")
Usage:
<Conversation className="flex-1 overflow-y-hidden">
<ConversationContent>
{/* Messages go here */}
</ConversationContent>
<ConversationScrollButton />
</Conversation>Features:
- Uses
use-stick-to-bottomfor intelligent scrolling - Automatically scrolls to bottom when new messages arrive
- Pauses auto-scroll when user scrolls up manually
- Provides context for scroll state to child components
- Sets
role="log"for accessibility
ConversationContent
Content area for messages within the conversation.
type ConversationContentProps = ComponentProps<typeof StickToBottom.Content>;Usage:
<ConversationContent className="flex flex-col gap-8 p-4">
{messages.map(message => (
<Message key={message.id} from={message.role}>
{/* Message content */}
</Message>
))}
</ConversationContent>Default Styling:
- Flexbox column layout with gap
- Padding for content separation
ConversationEmptyState
Placeholder shown when there are no messages.
type ConversationEmptyStateProps = ComponentProps<"div"> & {
title?: string;
description?: string;
icon?: React.ReactNode;
};Props:
title?: string- Heading text (default: "No messages yet")description?: string- Descriptive text (default: "Start a conversation to see messages here")icon?: React.ReactNode- Icon to display above textchildren?: React.ReactNode- Custom content (overrides default)
Usage:
{messages.length === 0 ? (
<ConversationEmptyState
title="Welcome!"
description="Ask me anything to get started"
icon={<MessageSquareIcon className="size-12" />}
/>
) : (
<ConversationContent>
{/* Messages */}
</ConversationContent>
)}ConversationScrollButton
Button that appears when user is not at the bottom of the conversation, allowing quick navigation to latest messages.
type ConversationScrollButtonProps = ComponentProps<typeof Button>;Usage:
<Conversation>
<ConversationContent>
{/* Messages */}
</ConversationContent>
<ConversationScrollButton />
</Conversation>Behavior:
- Only visible when
isAtBottomis false - Positioned at bottom center of conversation
- Calls
scrollToBottom()on click - Uses
ArrowDownIconby default
Message Components
Message
Container for an individual message with role-based styling.
type MessageProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"]; // "user" | "assistant"
};Props:
from: "user" | "assistant"- Message sender roleclassName?: string- Additional CSS classes- Standard HTML div attributes
Usage:
<Message from="assistant">
<MessageContent>
<MessageResponse>{content}</MessageResponse>
</MessageContent>
<MessageActions>
<MessageAction tooltip="Copy" onClick={copyToClipboard}>
<CopyIcon />
</MessageAction>
</MessageActions>
</Message>Styling:
- User messages: right-aligned, max-width 80%
- Assistant messages: left-aligned, max-width 80%
- Adds
is-useroris-assistantclass for context-specific styling
MessageContent
Content area for message text and media.
type MessageContentProps = HTMLAttributes<HTMLDivElement>;Usage:
<MessageContent>
<MessageResponse>{text}</MessageResponse>
</MessageContent>Styling:
- User messages: rounded background with secondary color
- Assistant messages: plain text styling
- Flexbox column layout for multiple content types
MessageResponse
Renders markdown/text content with streaming support.
type MessageResponseProps = ComponentProps<typeof Streamdown>;Usage:
<MessageResponse>
{message.content}
</MessageResponse>Features:
- Uses
Streamdownfor markdown rendering - Memoized to prevent unnecessary re-renders
- Supports streaming text updates
- Removes default margin from first/last children
MessageActions
Container for action buttons (copy, edit, regenerate, etc.).
type MessageActionsProps = ComponentProps<"div">;Usage:
<MessageActions>
<MessageAction tooltip="Copy" onClick={handleCopy}>
<CopyIcon />
</MessageAction>
<MessageAction tooltip="Regenerate" onClick={handleRegenerate}>
<RefreshIcon />
</MessageAction>
</MessageActions>MessageAction
Individual action button with optional tooltip.
type MessageActionProps = ComponentProps<typeof Button> & {
tooltip?: string;
label?: string;
};Props:
tooltip?: string- Tooltip text shown on hoverlabel?: string- Accessible label (falls back to tooltip)- All Button component props
Usage:
<MessageAction
tooltip="Copy to clipboard"
onClick={handleCopy}
variant="ghost"
size="icon-sm"
>
<CopyIcon className="size-4" />
</MessageAction>Message Branching
MessageBranch
Container for managing alternative message responses with navigation.
type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
defaultBranch?: number;
onBranchChange?: (branchIndex: number) => void;
};Props:
defaultBranch?: number- Initial branch index (default: 0)onBranchChange?: (index: number) => void- Callback when branch changes
Usage:
<MessageBranch defaultBranch={0} onBranchChange={handleBranchChange}>
<MessageBranchContent>
<MessageResponse key="1">{response1}</MessageResponse>
<MessageResponse key="2">{response2}</MessageResponse>
<MessageResponse key="3">{response3}</MessageResponse>
</MessageBranchContent>
<MessageBranchSelector from="assistant">
<MessageBranchPrevious />
<MessageBranchPage />
<MessageBranchNext />
</MessageBranchSelector>
</MessageBranch>Context: Provides context with:
currentBranch: number- Current branch indextotalBranches: number- Total number of branchesgoToPrevious: () => void- Navigate to previous branchgoToNext: () => void- Navigate to next branch
MessageBranchContent
Displays the current branch content, hiding others.
type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;Behavior:
- Automatically manages branch visibility
- Updates when children change
- Preserves all branches in DOM (display: none for hidden)
MessageBranchSelector
Container for branch navigation controls.
type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"];
};Behavior:
- Only renders if
totalBranches > 1 - Uses ButtonGroup for grouped appearance
MessageBranchPrevious
Button to navigate to previous branch.
type MessageBranchPreviousProps = ComponentProps<typeof Button>;Behavior:
- Wraps around (last branch → first branch)
- Disabled if only one branch exists
- Default icon:
ChevronLeftIcon
MessageBranchNext
Button to navigate to next branch.
type MessageBranchNextProps = ComponentProps<typeof Button>;Behavior:
- Wraps around (first branch → last branch)
- Disabled if only one branch exists
- Default icon:
ChevronRightIcon
MessageBranchPage
Displays current branch number and total.
type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;Display: Shows "1 of 3", "2 of 3", etc.
Attachment Components
MessageAttachment
Displays a file or image attachment with optional remove button.
type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
data: FileUIPart;
className?: string;
onRemove?: () => void;
};Props:
data: FileUIPart- Attachment data (url, filename, mediaType)onRemove?: () => void- Callback to remove attachment
Usage:
<MessageAttachment
data={{
type: "file",
url: "blob:...",
filename: "document.pdf",
mediaType: "application/pdf"
}}
onRemove={() => removeAttachment(id)}
/>Behavior:
- Images: Shows thumbnail preview
- Files: Shows paperclip icon
- Hover: Shows remove button (if onRemove provided)
- Tooltip: Displays filename on non-image files
MessageAttachments
Container for multiple attachments.
type MessageAttachmentsProps = ComponentProps<"div">;Usage:
<MessageAttachments>
{attachments.map(attachment => (
<MessageAttachment key={attachment.id} data={attachment} />
))}
</MessageAttachments>Styling:
- Flexbox wrap layout
- Right-aligned (ml-auto)
- Gap between items
MessageToolbar
Container for toolbar elements below message content.
type MessageToolbarProps = ComponentProps<"div">;Usage:
<MessageToolbar>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{timestamp}</span>
</div>
<MessageActions>
{/* Action buttons */}
</MessageActions>
</MessageToolbar>Complete Example
import {
Conversation,
ConversationContent,
ConversationEmptyState,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import {
Message,
MessageContent,
MessageResponse,
MessageActions,
MessageAction,
MessageAttachments,
MessageAttachment,
MessageBranch,
MessageBranchContent,
MessageBranchSelector,
MessageBranchPrevious,
MessageBranchNext,
MessageBranchPage,
} from "@/components/ai-elements/message";
function ChatInterface({ messages }: { messages: UIMessage[] }) {
return (
<Conversation className="flex-1">
{messages.length === 0 ? (
<ConversationEmptyState
title="Start a conversation"
description="Ask me anything!"
/>
) : (
<ConversationContent className="p-4">
{messages.map(message => (
<Message key={message.id} from={message.role}>
{message.attachments && (
<MessageAttachments>
{message.attachments.map(att => (
<MessageAttachment key={att.url} data={att} />
))}
</MessageAttachments>
)}
<MessageContent>
{message.branches ? (
<MessageBranch>
<MessageBranchContent>
{message.branches.map((branch, idx) => (
<MessageResponse key={idx}>{branch}</MessageResponse>
))}
</MessageBranchContent>
<MessageBranchSelector from={message.role}>
<MessageBranchPrevious />
<MessageBranchPage />
<MessageBranchNext />
</MessageBranchSelector>
</MessageBranch>
) : (
<MessageResponse>{message.content}</MessageResponse>
)}
</MessageContent>
<MessageActions>
<MessageAction tooltip="Copy" onClick={() => copy(message)}>
<CopyIcon />
</MessageAction>
<MessageAction tooltip="Regenerate" onClick={() => regenerate(message)}>
<RefreshIcon />
</MessageAction>
</MessageActions>
</Message>
))}
</ConversationContent>
)}
<ConversationScrollButton />
</Conversation>
);
}Prompt Input Components
Advanced text input components with file attachments, drag-and-drop, speech input, and comprehensive state management.
Table of Contents
- Core Components
- State Management
- Attachment Handling
- Action Menus
- Submit Button
- Speech Input
- Advanced Features
- Complete Example
Core Components
PromptInput
Main form container that handles text input, file attachments, and submission.
type PromptInputProps = Omit<HTMLAttributes<HTMLFormElement>, "onSubmit" | "onError"> & {
accept?: string;
multiple?: boolean;
globalDrop?: boolean;
syncHiddenInput?: boolean;
maxFiles?: number;
maxFileSize?: number;
onError?: (err: { code: "max_files" | "max_file_size" | "accept"; message: string }) => void;
onSubmit: (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => void | Promise<void>;
};
type PromptInputMessage = {
text: string;
files: FileUIPart[];
};Props:
accept?: string- File type filter (e.g., "image/*")multiple?: boolean- Allow multiple file selectionglobalDrop?: boolean- Accept drops anywhere on document (default: false)syncHiddenInput?: boolean- Keep hidden input in sync (default: false)maxFiles?: number- Maximum number of filesmaxFileSize?: number- Maximum file size in bytesonError?: (err) => void- Error handler for file validationonSubmit: (message, event) => void | Promise<void>- Submit handler (required)
Usage:
<PromptInput
accept="image/*"
multiple
maxFiles={5}
maxFileSize={10 * 1024 * 1024} // 10MB
onError={(err) => toast.error(err.message)}
onSubmit={async (message) => {
await sendMessage(message.text, message.files);
}}
>
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
<PromptInputBody>
<PromptInputTextarea placeholder="Type a message..." />
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputButton onClick={() => attachments.openFileDialog()}>
<PaperclipIcon />
</PromptInputButton>
</PromptInputTools>
<PromptInputSubmit status={chatStatus} />
</PromptInputFooter>
</PromptInput>Features:
- Dual-mode operation (controlled/uncontrolled)
- Drag-and-drop file handling (local or global)
- Paste image/file support
- File validation (type, size, count)
- Automatic blob URL to data URL conversion
- Async/sync onSubmit support
- Auto-reset on successful submission
PromptInputBody
Container for the main input area.
type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>;Usage:
<PromptInputBody>
<PromptInputTextarea />
</PromptInputBody>PromptInputTextarea
Auto-expanding textarea with keyboard shortcuts and paste handling.
type PromptInputTextareaProps = ComponentProps<typeof InputGroupTextarea>;Props:
placeholder?: string- Placeholder text (default: "What would you like to know?")- Standard textarea props
Usage:
<PromptInputTextarea
placeholder="Ask me anything..."
className="max-h-48 min-h-16"
/>Keyboard Shortcuts:
Enter- Submit (without Shift)Shift+Enter- New lineBackspace- Remove last attachment when textarea is empty
Features:
- Auto-expands with content (field-sizing-content)
- Paste image/file support
- Composition event handling (for IME)
- Respects submit button disabled state
- Controlled/uncontrolled dual-mode
PromptInputHeader
Header section for additional controls above the textarea.
type PromptInputHeaderProps = Omit<ComponentProps<typeof InputGroupAddon>, "align">;Usage:
<PromptInputHeader>
<PromptInputSelect>
<PromptInputSelectTrigger>
<PromptInputSelectValue placeholder="Select model" />
</PromptInputSelectTrigger>
<PromptInputSelectContent>
<PromptInputSelectItem value="gpt-4">GPT-4</PromptInputSelectItem>
<PromptInputSelectItem value="claude">Claude</PromptInputSelectItem>
</PromptInputSelectContent>
</PromptInputSelect>
</PromptInputHeader>PromptInputFooter
Footer section for tools and submit button.
type PromptInputFooterProps = Omit<ComponentProps<typeof InputGroupAddon>, "align">;Usage:
<PromptInputFooter>
<PromptInputTools>
{/* Tool buttons */}
</PromptInputTools>
<PromptInputSubmit status="ready" />
</PromptInputFooter>PromptInputTools
Container for tool buttons in the footer.
type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;Usage:
<PromptInputTools>
<PromptInputButton onClick={openFileDialog}>
<PaperclipIcon />
</PromptInputButton>
<PromptInputSpeechButton textareaRef={textareaRef} />
</PromptInputTools>PromptInputButton
Generic button for actions and tools.
type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;Props:
variant?: ButtonVariant- Button style (default: "ghost")size?: ButtonSize- Button size (auto-determined based on children)
Usage:
<PromptInputButton onClick={handleAction}>
<IconComponent />
</PromptInputButton>
<PromptInputButton onClick={handleAction}>
<IconComponent />
Label
</PromptInputButton>State Management
PromptInputProvider
Optional global provider that lifts input and attachment state outside of PromptInput.
type PromptInputProviderProps = PropsWithChildren<{
initialInput?: string;
}>;Usage:
<PromptInputProvider initialInput="">
{/* App content */}
<PromptInput onSubmit={handleSubmit}>
{/* Input content */}
</PromptInput>
{/* External components can access state */}
<ExternalComponent />
</PromptInputProvider>Provides:
textInput: TextInputContext- Text state and settersattachments: AttachmentsContext- File state and methods__registerFileInput- Internal registration method
usePromptInputController
Hook to access the provider state.
const usePromptInputController = () => {
const { textInput, attachments } = usePromptInputController();
return {
textInput: {
value: string;
setInput: (v: string) => void;
clear: () => void;
},
attachments: {
files: (FileUIPart & { id: string })[];
add: (files: File[] | FileList) => void;
remove: (id: string) => void;
clear: () => void;
openFileDialog: () => void;
fileInputRef: RefObject<HTMLInputElement | null>;
}
};
};Usage:
function ExternalComponent() {
const { textInput, attachments } = usePromptInputController();
return (
<div>
<p>Current input: {textInput.value}</p>
<p>Attachments: {attachments.files.length}</p>
<Button onClick={() => textInput.clear()}>Clear</Button>
</div>
);
}useProviderAttachments
Hook to access attachment state from provider.
const useProviderAttachments = () => AttachmentsContext;usePromptInputAttachments
Hook to access attachment state (dual-mode: provider or local).
const usePromptInputAttachments = () => AttachmentsContext;Attachment Handling
PromptInputAttachments
Container for rendering attachments.
type PromptInputAttachmentsProps = Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
children: (attachment: FileUIPart & { id: string }) => ReactNode;
};Usage:
<PromptInputAttachments>
{(attachment) => (
<PromptInputAttachment data={attachment} />
)}
</PromptInputAttachments>Behavior:
- Only renders if attachments exist
- Uses render prop pattern for flexibility
PromptInputAttachment
Individual attachment display with preview and remove button.
type PromptInputAttachmentProps = HTMLAttributes<HTMLDivElement> & {
data: FileUIPart & { id: string };
className?: string;
};Usage:
<PromptInputAttachment
data={{
id: "abc123",
type: "file",
url: "blob:...",
filename: "image.png",
mediaType: "image/png"
}}
/>Features:
- Image preview for image/* media types
- Paperclip icon for other files
- Hover to reveal remove button
- Hover card with full preview
- Truncated filename display
PromptInputHoverCard
Hover card for attachment preview.
type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>;Default Delays:
openDelay: 0- Instant opencloseDelay: 0- Instant close
PromptInputHoverCardContent
Content area for hover card preview.
type PromptInputHoverCardContentProps = ComponentProps<typeof HoverCardContent>;Action Menus
PromptInputActionMenu
Dropdown menu for additional actions.
type PromptInputActionMenuProps = ComponentProps<typeof DropdownMenu>;Usage:
<PromptInputActionMenu>
<PromptInputActionMenuTrigger>
<PlusIcon />
</PromptInputActionMenuTrigger>
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments label="Add files" />
<PromptInputActionMenuItem>
<SettingsIcon className="mr-2 size-4" />
Settings
</PromptInputActionMenuItem>
</PromptInputActionMenuContent>
</PromptInputActionMenu>PromptInputActionMenuTrigger
Trigger button for action menu.
type PromptInputActionMenuTriggerProps = PromptInputButtonProps;Default Icon: PlusIcon
PromptInputActionMenuContent
Content area for menu items.
type PromptInputActionMenuContentProps = ComponentProps<typeof DropdownMenuContent>;Default Alignment: align="start"
PromptInputActionMenuItem
Individual menu item.
type PromptInputActionMenuItemProps = ComponentProps<typeof DropdownMenuItem>;PromptInputActionAddAttachments
Pre-built menu item for adding attachments.
type PromptInputActionAddAttachmentsProps = ComponentProps<typeof DropdownMenuItem> & {
label?: string;
};Props:
label?: string- Button label (default: "Add photos or files")
Usage:
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments label="Upload images" />
</PromptInputActionMenuContent>Submit Button
PromptInputSubmit
Status-aware submit button with dynamic icons.
type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
status?: ChatStatus; // "submitted" | "streaming" | "error" | "ready"
};Status Icons:
undefined/"ready"-CornerDownLeftIcon(enter key)"submitted"-Loader2Icon(spinning)"streaming"-SquareIcon(stop)"error"-XIcon(error)
Usage:
<PromptInputSubmit status={chatStatus} />Behavior:
type="submit"- Triggers form submissionaria-label="Submit"- Accessible label
Speech Input
PromptInputSpeechButton
Voice input button using Web Speech API.
type PromptInputSpeechButtonProps = ComponentProps<typeof PromptInputButton> & {
textareaRef?: RefObject<HTMLTextAreaElement | null>;
onTranscriptionChange?: (text: string) => void;
};Props:
textareaRef?: RefObject- Reference to textarea for text insertiononTranscriptionChange?: (text: string) => void- Callback when text changes
Usage:
const textareaRef = useRef<HTMLTextAreaElement>(null);
<PromptInputTextarea ref={textareaRef} />
<PromptInputSpeechButton
textareaRef={textareaRef}
onTranscriptionChange={(text) => console.log("Transcribed:", text)}
/>Features:
- Browser-based speech recognition
- Continuous recording mode
- Interim results support
- Automatic text insertion
- Visual feedback (pulse animation when listening)
- Disabled if browser doesn't support Speech Recognition
- Error handling
Advanced Features
Select Components
For model selection or other dropdowns.
<PromptInputSelect value={model} onValueChange={setModel}>
<PromptInputSelectTrigger>
<PromptInputSelectValue placeholder="Select model" />
</PromptInputSelectTrigger>
<PromptInputSelectContent>
<PromptInputSelectItem value="gpt-4">GPT-4</PromptInputSelectItem>
<PromptInputSelectItem value="claude">Claude</PromptInputSelectItem>
</PromptInputSelectContent>
</PromptInputSelect>Command Components
For slash commands or autocomplete.
<PromptInputCommand>
<PromptInputCommandInput placeholder="Search commands..." />
<PromptInputCommandList>
<PromptInputCommandEmpty>No commands found</PromptInputCommandEmpty>
<PromptInputCommandGroup heading="Actions">
<PromptInputCommandItem value="summarize">
Summarize
</PromptInputCommandItem>
<PromptInputCommandItem value="translate">
Translate
</PromptInputCommandItem>
</PromptInputCommandGroup>
<PromptInputCommandSeparator />
<PromptInputCommandGroup heading="Settings">
<PromptInputCommandItem value="preferences">
Preferences
</PromptInputCommandItem>
</PromptInputCommandGroup>
</PromptInputCommandList>
</PromptInputCommand>Tab Components
For organizing input modes or templates.
<PromptInputTabsList>
<PromptInputTab>
<PromptInputTabLabel>Templates</PromptInputTabLabel>
<PromptInputTabBody>
<PromptInputTabItem>Summarize article</PromptInputTabItem>
<PromptInputTabItem>Write email</PromptInputTabItem>
</PromptInputTabBody>
</PromptInputTab>
</PromptInputTabsList>Complete Example
import { useRef, useState } from "react";
import {
PromptInput,
PromptInputProvider,
PromptInputAttachments,
PromptInputAttachment,
PromptInputBody,
PromptInputTextarea,
PromptInputFooter,
PromptInputTools,
PromptInputButton,
PromptInputSpeechButton,
PromptInputSubmit,
PromptInputActionMenu,
PromptInputActionMenuTrigger,
PromptInputActionMenuContent,
PromptInputActionAddAttachments,
usePromptInputAttachments,
} from "@/components/ai-elements/prompt-input";
import { PaperclipIcon, PlusIcon } from "lucide-react";
function ChatInput() {
const [status, setStatus] = useState<ChatStatus>("ready");
const textareaRef = useRef<HTMLTextAreaElement>(null);
const attachments = usePromptInputAttachments();
const handleSubmit = async (message: PromptInputMessage) => {
setStatus("submitted");
try {
await sendMessage(message.text, message.files);
setStatus("ready");
} catch (error) {
setStatus("error");
}
};
return (
<PromptInput
accept="image/*,.pdf,.doc,.docx"
multiple
maxFiles={10}
maxFileSize={10 * 1024 * 1024}
onError={(err) => toast.error(err.message)}
onSubmit={handleSubmit}
>
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
<PromptInputBody>
<PromptInputTextarea
ref={textareaRef}
placeholder="Type a message..."
/>
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputButton onClick={() => attachments.openFileDialog()}>
<PaperclipIcon className="size-4" />
</PromptInputButton>
<PromptInputSpeechButton textareaRef={textareaRef} />
<PromptInputActionMenu>
<PromptInputActionMenuTrigger>
<PlusIcon className="size-4" />
</PromptInputActionMenuTrigger>
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
<PromptInputActionMenuItem>
Insert template
</PromptInputActionMenuItem>
</PromptInputActionMenuContent>
</PromptInputActionMenu>
</PromptInputTools>
<PromptInputSubmit status={status} />
</PromptInputFooter>
</PromptInput>
);
}
// With global provider
function App() {
return (
<PromptInputProvider initialInput="">
<ChatInput />
</PromptInputProvider>
);
}Visualization Components
ReactFlow-based components for workflow visualization, custom nodes, and animated edges.
Core Components
Canvas
ReactFlow wrapper with aviation-specific defaults and background.
type CanvasProps = ReactFlowProps & {
children?: ReactNode;
};Usage:
import { Canvas } from "@/components/ai-elements/canvas";
import { Background, Controls, Panel } from "@xyflow/react";
import "@xyflow/react/dist/style.css";
<Canvas
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
>
<Background bgColor="var(--sidebar)" />
<Controls />
<Panel position="top-left">
<h3>Workflow</h3>
</Panel>
</Canvas>Default Props:
deleteKeyCode: ["Backspace", "Delete"]- Keys to delete selected elementsfitView: true- Automatically fit content in viewportpanOnDrag: false- Disable pan on drag (use selection instead)panOnScroll: true- Enable pan on scrollselectionOnDrag: true- Enable selection box on dragzoomOnDoubleClick: false- Disable zoom on double click
Features:
- Includes Background component with sidebar color
- Accepts all ReactFlow props
- Optimized for workflow visualization
Node Components
Custom node components built on shadcn/ui Card components.
Node
Main node container with connection handles.
type NodeProps = ComponentProps<typeof Card> & {
handles: {
target: boolean;
source: boolean;
};
};Props:
handles: { target: boolean; source: boolean }- Which handles to show (required)- All Card component props
Usage:
<Node handles={{ target: true, source: true }}>
<NodeHeader>
<NodeTitle>Process Data</NodeTitle>
<NodeDescription>Transform input data</NodeDescription>
</NodeHeader>
<NodeContent>
{/* Node content */}
</NodeContent>
<NodeFooter>
Status: Running
</NodeFooter>
</Node>Handle Positions:
- Target: Left side (incoming connections)
- Source: Right side (outgoing connections)
Default Styling:
- Relative positioning for handles
- Auto height
- Fixed width (w-sm)
- Rounded corners
NodeHeader
Header section with border bottom and secondary background.
type NodeHeaderProps = ComponentProps<typeof CardHeader>;Usage:
<NodeHeader>
<NodeTitle>Step 1</NodeTitle>
<NodeDescription>Initial processing</NodeDescription>
<NodeAction onClick={handleEdit}>
<EditIcon />
</NodeAction>
</NodeHeader>Default Styling:
- Rounded top corners
- Border bottom
- Secondary background
- Compact padding (p-3)
NodeTitle
Title text for node header.
type NodeTitleProps = ComponentProps<typeof CardTitle>;Usage:
<NodeTitle>Data Processing</NodeTitle>NodeDescription
Secondary description text.
type NodeDescriptionProps = ComponentProps<typeof CardDescription>;Usage:
<NodeDescription>
Transform and validate input data
</NodeDescription>NodeAction
Action button in header (typically edit/delete).
type NodeActionProps = ComponentProps<typeof CardAction>;Usage:
<NodeAction onClick={handleEdit}>
<EditIcon className="size-4" />
</NodeAction>NodeContent
Main content area of the node.
type NodeContentProps = ComponentProps<typeof CardContent>;Usage:
<NodeContent>
<div className="space-y-2">
<Label>Input</Label>
<Input value={data.input} readOnly />
<Label>Output</Label>
<Input value={data.output} readOnly />
</div>
</NodeContent>Default Styling:
- Compact padding (p-3)
NodeFooter
Footer section with border top and secondary background.
type NodeFooterProps = ComponentProps<typeof CardFooter>;Usage:
<NodeFooter>
<Badge variant="success">Completed</Badge>
<span className="text-xs text-muted-foreground">
Duration: 1.2s
</span>
</NodeFooter>Default Styling:
- Rounded bottom corners
- Border top
- Secondary background
- Compact padding (p-3)
Edge Components
Custom edge types for different connection styles.
Edge.Temporary
Dashed edge for temporary or preview connections.
type TemporaryEdgeProps = EdgeProps;Usage:
const edgeTypes = {
temporary: Edge.Temporary,
};
<Canvas edges={edges} edgeTypes={edgeTypes} />Features:
- Simple Bezier curve
- Dashed stroke (5, 5)
- Ring color stroke
- Stroke width: 1
Use Cases:
- Drag preview connections
- Temporary workflow paths
- Suggested connections
Edge.Animated
Animated edge with moving dot indicator.
type AnimatedEdgeProps = EdgeProps;Usage:
const edgeTypes = {
animated: Edge.Animated,
};
<Canvas edges={edges} edgeTypes={edgeTypes} />Features:
- Bezier curve path
- Animated circle following edge path
- 2-second animation duration
- Infinite repeat
- Primary color dot (4px radius)
Use Cases:
- Active data flow
- Processing pipelines
- Real-time connections
Implementation Details:
- Uses
useInternalNodeto get node positions - Calculates handle coordinates based on position
- Supports Left (target) and Right (source) handle positions
- Uses
getBezierPathfor smooth curves
ReactFlow Integration
Controls
Standard ReactFlow controls (zoom, fit view, etc.).
import { Controls } from "@xyflow/react";Usage:
<Canvas>
<Controls />
</Canvas>Panel
Panel for custom UI overlays.
import { Panel } from "@xyflow/react";Usage:
<Canvas>
<Panel position="top-left">
<h3>Workflow Name</h3>
<p>Status: Running</p>
</Panel>
<Panel position="bottom-right">
<Button onClick={handleSave}>Save</Button>
</Panel>
</Canvas>Positions:
top-left,top-center,top-rightbottom-left,bottom-center,bottom-right
Background
Background pattern for the canvas.
import { Background } from "@xyflow/react";Usage:
<Canvas>
<Background bgColor="var(--sidebar)" />
</Canvas>Custom Node Types
Example of creating custom node types with aviation-specific styling.
import { Node, NodeHeader, NodeTitle, NodeContent, NodeFooter } from "@/components/ai-elements/node";
import type { NodeProps } from "@xyflow/react";
type ProcessNodeData = {
label: string;
status: "pending" | "running" | "completed" | "failed";
input?: string;
output?: string;
};
function ProcessNode({ data }: NodeProps<ProcessNodeData>) {
const statusColors = {
pending: "bg-gray-500",
running: "bg-blue-500 animate-pulse",
completed: "bg-green-500",
failed: "bg-red-500",
};
return (
<Node handles={{ target: true, source: true }}>
<NodeHeader>
<NodeTitle>{data.label}</NodeTitle>
</NodeHeader>
<NodeContent>
{data.input && (
<div className="space-y-1">
<Label className="text-xs">Input</Label>
<p className="text-xs text-muted-foreground">{data.input}</p>
</div>
)}
{data.output && (
<div className="space-y-1">
<Label className="text-xs">Output</Label>
<p className="text-xs text-muted-foreground">{data.output}</p>
</div>
)}
</NodeContent>
<NodeFooter>
<div className="flex items-center gap-2">
<div className={cn("size-2 rounded-full", statusColors[data.status])} />
<span className="text-xs capitalize">{data.status}</span>
</div>
</NodeFooter>
</Node>
);
}
// Register custom node type
const nodeTypes = {
process: ProcessNode,
};
<Canvas nodeTypes={nodeTypes} />Complete Example
import { useState } from "react";
import { Canvas } from "@/components/ai-elements/canvas";
import {
Node,
NodeHeader,
NodeTitle,
NodeDescription,
NodeContent,
NodeFooter,
} from "@/components/ai-elements/node";
import { Edge } from "@/components/ai-elements/edge";
import {
Background,
Controls,
Panel,
useNodesState,
useEdgesState,
addEdge,
type Connection,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
const initialNodes = [
{
id: "1",
type: "custom",
position: { x: 0, y: 0 },
data: { label: "Start", status: "completed" },
},
{
id: "2",
type: "custom",
position: { x: 250, y: 0 },
data: { label: "Process", status: "running" },
},
{
id: "3",
type: "custom",
position: { x: 500, y: 0 },
data: { label: "End", status: "pending" },
},
];
const initialEdges = [
{
id: "e1-2",
source: "1",
target: "2",
type: "animated",
},
{
id: "e2-3",
source: "2",
target: "3",
type: "temporary",
},
];
function CustomNode({ data }) {
return (
<Node handles={{ target: true, source: true }}>
<NodeHeader>
<NodeTitle>{data.label}</NodeTitle>
<NodeDescription>Step in workflow</NodeDescription>
</NodeHeader>
<NodeContent>
<p className="text-sm">Status: {data.status}</p>
</NodeContent>
<NodeFooter>
<Badge variant={data.status === "completed" ? "success" : "default"}>
{data.status}
</Badge>
</NodeFooter>
</Node>
);
}
const nodeTypes = {
custom: CustomNode,
};
const edgeTypes = {
temporary: Edge.Temporary,
animated: Edge.Animated,
};
function WorkflowCanvas() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (connection: Connection) => {
setEdges((eds) => addEdge({ ...connection, type: "animated" }, eds));
};
return (
<div className="h-screen">
<Canvas
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
>
<Background bgColor="var(--sidebar)" />
<Controls />
<Panel position="top-left">
<div className="rounded-lg bg-background p-4 shadow-lg">
<h3 className="font-semibold">Workflow</h3>
<p className="text-sm text-muted-foreground">
{nodes.length} nodes, {edges.length} edges
</p>
</div>
</Panel>
</Canvas>
</div>
);
}Handle Positioning
The edge components use custom handle coordinate calculation:
const getHandleCoordsByPosition = (node, handlePosition) => {
const handleType = handlePosition === Position.Left ? "target" : "source";
const handle = node.internals.handleBounds?.[handleType]?.find(
(h) => h.position === handlePosition
);
// Calculate absolute coordinates
const offsetX = handlePosition === Position.Right ? handle.width : 0;
const offsetY = handlePosition === Position.Bottom ? handle.height : 0;
const x = node.internals.positionAbsolute.x + handle.x + offsetX;
const y = node.internals.positionAbsolute.y + handle.y + offsetY;
return [x, y];
};This ensures edges connect properly to handle centers.
Styling Tips
Node Widths
Control node width with Tailwind classes:
<Node handles={handles} className="w-64">
{/* wider node */}
</Node>
<Node handles={handles} className="w-96">
{/* extra wide node */}
</Node>Edge Colors
Customize edge colors via className:
// In custom edge component
<BaseEdge
className="stroke-2 stroke-primary"
id={id}
path={edgePath}
/>Node Status Indicators
Add visual status indicators:
<NodeFooter>
<div className="flex items-center gap-2">
<div className={cn(
"size-2 rounded-full",
status === "running" && "bg-blue-500 animate-pulse",
status === "completed" && "bg-green-500",
status === "failed" && "bg-red-500"
)} />
<span className="text-xs">{status}</span>
</div>
</NodeFooter>Integration with Aviation Nodes
AI Elements Canvas integrates seamlessly with custom aviation-specific node types. The Node component provides a flexible base for domain-specific extensions:
// Aviation-specific node
function FlightPlanNode({ data }) {
return (
<Node handles={{ target: true, source: true }}>
<NodeHeader>
<NodeTitle>{data.flightNumber}</NodeTitle>
<NodeDescription>{data.route}</NodeDescription>
</NodeHeader>
<NodeContent>
<div className="space-y-2 text-xs">
<div>Departure: {data.departure}</div>
<div>Arrival: {data.arrival}</div>
<div>Aircraft: {data.aircraft}</div>
</div>
</NodeContent>
<NodeFooter>
<Badge>{data.status}</Badge>
</NodeFooter>
</Node>
);
}Workflow Components
Components for displaying job queues, tool execution, approval workflows, and reasoning displays.
Table of Contents
Queue Components
Components for displaying task lists, job queues, and progress tracking.
Queue
Main container for queue items and sections.
type QueueProps = ComponentProps<"div">;Usage:
<Queue>
<QueueSection>
{/* Queue items */}
</QueueSection>
</Queue>Default Styling:
- Bordered container with rounded corners
- Background with shadow
- Flexbox column layout with gap
QueueSection
Collapsible section for organizing queue items.
type QueueSectionProps = ComponentProps<typeof Collapsible>;Props:
defaultOpen?: boolean- Initial open state (default: true)
Usage:
<QueueSection defaultOpen={true}>
<QueueSectionTrigger>
<QueueSectionLabel count={5} label="pending tasks" />
</QueueSectionTrigger>
<QueueSectionContent>
<QueueList>
{/* Queue items */}
</QueueList>
</QueueSectionContent>
</QueueSection>QueueSectionTrigger
Clickable header to toggle section visibility.
type QueueSectionTriggerProps = ComponentProps<"button">;Default Styling:
- Full width button
- Muted background with hover effect
- Flexbox layout for content alignment
QueueSectionLabel
Label with icon, text, and count display.
type QueueSectionLabelProps = ComponentProps<"span"> & {
count?: number;
label: string;
icon?: React.ReactNode;
};Props:
count?: number- Item count to displaylabel: string- Section label (required)icon?: React.ReactNode- Optional icon
Usage:
<QueueSectionLabel
count={todos.length}
label="todos"
icon={<CheckSquareIcon className="size-4" />}
/>Display: Shows "{count} {label}" with chevron icon that rotates when expanded.
QueueSectionContent
Collapsible content area for queue items.
type QueueSectionContentProps = ComponentProps<typeof CollapsibleContent>;QueueList
Scrollable container for queue items.
type QueueListProps = ComponentProps<typeof ScrollArea>;Default Styling:
- Max height of 40 (10rem)
- Scrollable when content overflows
- Padding for scroll area
Usage:
<QueueList>
{items.map(item => (
<QueueItem key={item.id}>
{/* Item content */}
</QueueItem>
))}
</QueueList>QueueItem
Individual queue item container.
type QueueItemProps = ComponentProps<"li">;Usage:
<QueueItem>
<QueueItemIndicator completed={todo.status === 'completed'} />
<QueueItemContent completed={todo.status === 'completed'}>
{todo.title}
</QueueItemContent>
{todo.description && (
<QueueItemDescription completed={todo.status === 'completed'}>
{todo.description}
</QueueItemDescription>
)}
<QueueItemActions>
<QueueItemAction onClick={handleEdit}>
<EditIcon />
</QueueItemAction>
</QueueItemActions>
</QueueItem>Default Styling:
- Flexbox column layout
- Hover background effect
- Grouped item styling
QueueItemIndicator
Status indicator dot.
type QueueItemIndicatorProps = ComponentProps<"span"> & {
completed?: boolean;
};Props:
completed?: boolean- Whether item is completed (default: false)
Styling:
- Pending: Solid border
- Completed: Muted border and background
QueueItemContent
Main content text for queue item.
type QueueItemContentProps = ComponentProps<"span"> & {
completed?: boolean;
};Props:
completed?: boolean- Whether item is completed (default: false)
Styling:
- Pending: Normal text
- Completed: Muted text with line-through
QueueItemDescription
Secondary description text.
type QueueItemDescriptionProps = ComponentProps<"div"> & {
completed?: boolean;
};Props:
completed?: boolean- Whether item is completed (default: false)
Styling:
- Smaller text size
- Indented under main content
- Muted color (more muted if completed)
QueueItemActions
Container for action buttons.
type QueueItemActionsProps = ComponentProps<"div">;Usage:
<QueueItemActions>
<QueueItemAction onClick={handleEdit}>
<EditIcon />
</QueueItemAction>
<QueueItemAction onClick={handleDelete}>
<TrashIcon />
</QueueItemAction>
</QueueItemActions>QueueItemAction
Individual action button.
type QueueItemActionProps = Omit<ComponentProps<typeof Button>, "variant" | "size">;Behavior:
- Hidden by default
- Visible on item hover
- Ghost variant
- Icon size
QueueItemAttachment
Container for attached files/images.
type QueueItemAttachmentProps = ComponentProps<"div">;QueueItemImage
Image preview thumbnail.
type QueueItemImageProps = ComponentProps<"img">;Default Size: 32x32px
QueueItemFile
File attachment display with icon.
type QueueItemFileProps = ComponentProps<"span">;Features:
- Paperclip icon
- Truncated filename (max 100px)
- Border and background styling
Tool Components
Components for displaying tool execution with states, parameters, and results.
Tool
Main container for tool execution display.
type ToolProps = ComponentProps<typeof Collapsible>;Usage:
<Tool>
<ToolHeader
title="search"
type="tool-call-search"
state="output-available"
/>
<ToolContent>
<ToolInput input={{ query: "AI tools" }} />
<ToolOutput output={results} errorText={undefined} />
</ToolContent>
</Tool>Default Styling:
- Bordered container
- Rounded corners
- Not prose (for content formatting)
ToolHeader
Collapsible trigger showing tool name and status.
type ToolHeaderProps = {
title?: string;
type: ToolUIPart["type"];
state: ToolUIPart["state"];
className?: string;
};Props:
title?: string- Display name (defaults to type without "tool-call-" prefix)type: string- Tool type identifierstate: ToolState- Current execution state (required)
Tool States:
input-streaming- Parameters being received (Pending badge)input-available- Ready to execute (Running badge, pulsing)approval-requested- Awaiting approval (Awaiting Approval badge, yellow)approval-responded- User responded (Responded badge, blue)output-available- Completed (Completed badge, green)output-error- Failed (Error badge, red)output-denied- Approval denied (Denied badge, orange)
Features:
- Wrench icon
- Color-coded status badge
- Chevron that rotates when expanded
ToolContent
Collapsible content area for parameters and results.
type ToolContentProps = ComponentProps<typeof CollapsibleContent>;Animation:
- Slide in/out from top
- Fade transition
ToolInput
Displays tool parameters/arguments.
type ToolInputProps = ComponentProps<"div"> & {
input: ToolUIPart["input"];
};Props:
input: unknown- Tool parameters (any JSON-serializable value)
Usage:
<ToolInput
input={{
query: "AI tools",
limit: 10,
filters: ["type:library"]
}}
/>Features:
- "PARAMETERS" heading
- JSON syntax highlighting via CodeBlock
- Automatic JSON.stringify with formatting
ToolOutput
Displays tool results or errors.
type ToolOutputProps = ComponentProps<"div"> & {
output: ToolUIPart["output"];
errorText: ToolUIPart["errorText"];
};Props:
output: unknown- Tool result (any value, React element, or JSON)errorText?: string- Error message if execution failed
Usage:
<ToolOutput
output={{
results: [...],
count: 42
}}
errorText={undefined}
/>Behavior:
- Shows "RESULT" heading for success
- Shows "ERROR" heading for errors
- Renders React elements directly
- JSON stringifies objects
- CodeBlock for strings
- Destructive styling for errors
Confirmation Components
Components for approval workflows requiring user confirmation.
Confirmation
Container for confirmation UI with conditional rendering based on state.
type ConfirmationProps = ComponentProps<typeof Alert> & {
approval?: ToolUIPartApproval;
state: ToolUIPart["state"];
};
type ToolUIPartApproval =
| { id: string; approved?: never; reason?: never }
| { id: string; approved: boolean; reason?: string }
| undefined;Props:
approval?: ToolUIPartApproval- Approval datastate: ToolState- Current state
Usage:
<Confirmation approval={tool.approval} state={tool.state}>
<ConfirmationTitle>
Delete {count} files?
</ConfirmationTitle>
<ConfirmationRequest>
<ConfirmationActions>
<ConfirmationAction onClick={handleApprove} variant="default">
Approve
</ConfirmationAction>
<ConfirmationAction onClick={handleReject} variant="outline">
Reject
</ConfirmationAction>
</ConfirmationActions>
</ConfirmationRequest>
<ConfirmationAccepted>
Action approved and executed.
</ConfirmationAccepted>
<ConfirmationRejected>
Action rejected.
</ConfirmationRejected>
</Confirmation>Context: Provides { approval, state } to child components.
ConfirmationTitle
Title/description of the confirmation request.
type ConfirmationTitleProps = ComponentProps<typeof AlertDescription>;ConfirmationRequest
Container shown during approval-requested state.
type ConfirmationRequestProps = { children?: ReactNode };Visibility: Only shown when state === "approval-requested"
ConfirmationActions
Container for approve/reject buttons.
type ConfirmationActionsProps = ComponentProps<"div">;Visibility: Only shown when state === "approval-requested"
ConfirmationAction
Individual action button.
type ConfirmationActionProps = ComponentProps<typeof Button>;Default Styling:
- Small height (h-8)
- Compact padding
ConfirmationAccepted
Content shown when approval is accepted.
type ConfirmationAcceptedProps = { children?: ReactNode };Visibility: Only shown when approval.approved === true and state is response/output state.
ConfirmationRejected
Content shown when approval is rejected.
type ConfirmationRejectedProps = { children?: ReactNode };Visibility: Only shown when approval.approved === false and state is response/output state.
useConfirmation
Hook to access confirmation context.
const useConfirmation = () => {
const { approval, state } = useConfirmation();
// ...
};Reasoning Components
Components for displaying AI thinking/reasoning with auto-collapse behavior.
Reasoning
Collapsible container for reasoning content with auto-collapse.
type ReasoningProps = ComponentProps<typeof Collapsible> & {
isStreaming?: boolean;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
duration?: number;
};Props:
isStreaming?: boolean- Whether content is actively streaming (default: false)open?: boolean- Controlled open statedefaultOpen?: boolean- Initial open state (default: true)onOpenChange?: (open: boolean) => void- Callback when open state changesduration?: number- Thinking duration in seconds
Usage:
<Reasoning isStreaming={isStreaming} defaultOpen={true}>
<ReasoningTrigger />
<ReasoningContent>
{reasoningText}
</ReasoningContent>
</Reasoning>Auto-Collapse Behavior: 1. Opens automatically when streaming starts 2. Tracks duration from start to end of streaming 3. Closes 1 second after streaming ends 4. Auto-close only happens once per component lifecycle
Context: Provides { isStreaming, isOpen, setIsOpen, duration } to children.
ReasoningTrigger
Trigger button with status message and icon.
type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
};Props:
getThinkingMessage?: (isStreaming, duration) => ReactNode- Custom message generator
Default Messages:
- Streaming: "Thinking..." with shimmer effect
- Duration 0 or undefined: "Thought for a few seconds"
- Duration N: "Thought for N seconds"
Usage:
<ReasoningTrigger />
// Custom message
<ReasoningTrigger
getThinkingMessage={(streaming, duration) =>
streaming ? <Shimmer>Processing...</Shimmer> : `Done in ${duration}s`
}
/>Icons:
- Brain icon
- Rotating chevron (down when closed, up when open)
ReasoningContent
Collapsible content area with markdown rendering.
type ReasoningContentProps = ComponentProps<typeof CollapsibleContent> & {
children: string;
};Props:
children: string- Reasoning text (required, string only)
Features:
- Uses Streamdown for markdown rendering
- Slide and fade animations
- Muted text color
useReasoning
Hook to access reasoning context.
const useReasoning = () => {
const { isStreaming, isOpen, setIsOpen, duration } = useReasoning();
// ...
};Loading Components
Shimmer
Animated shimmer effect for loading text.
type TextShimmerProps = {
children: string;
as?: ElementType;
className?: string;
duration?: number;
spread?: number;
};Props:
children: string- Text to shimmer (required)as?: ElementType- HTML element type (default: "p")className?: string- Additional classesduration?: number- Animation duration in seconds (default: 2)spread?: number- Shimmer spread multiplier (default: 2)
Usage:
<Shimmer duration={1.5}>Thinking...</Shimmer>
<Shimmer as="span" spread={3}>Loading data...</Shimmer>Features:
- Framer Motion animation
- Gradient sweep effect
- Dynamic spread based on text length
- Infinite loop
Loader
Spinning loader icon.
type LoaderProps = HTMLAttributes<HTMLDivElement> & {
size?: number;
};Props:
size?: number- Icon size in pixels (default: 16)
Usage:
<Loader size={24} />
<Loader className="text-primary" />Features:
- SVG-based spinner
- CSS animation (spin)
- Respects current color
Complete Example
import {
Queue,
QueueSection,
QueueSectionTrigger,
QueueSectionLabel,
QueueSectionContent,
QueueList,
QueueItem,
QueueItemIndicator,
QueueItemContent,
QueueItemDescription,
} from "@/components/ai-elements/queue";
import {
Tool,
ToolHeader,
ToolContent,
ToolInput,
ToolOutput,
} from "@/components/ai-elements/tool";
import {
Confirmation,
ConfirmationTitle,
ConfirmationRequest,
ConfirmationActions,
ConfirmationAction,
ConfirmationAccepted,
ConfirmationRejected,
} from "@/components/ai-elements/confirmation";
import {
Reasoning,
ReasoningTrigger,
ReasoningContent,
} from "@/components/ai-elements/reasoning";
function WorkflowDisplay({ todos, tools, reasoning }) {
return (
<div className="space-y-4">
{/* Queue */}
<Queue>
<QueueSection>
<QueueSectionTrigger>
<QueueSectionLabel count={todos.length} label="tasks" />
</QueueSectionTrigger>
<QueueSectionContent>
<QueueList>
{todos.map(todo => (
<QueueItem key={todo.id}>
<QueueItemIndicator completed={todo.completed} />
<QueueItemContent completed={todo.completed}>
{todo.title}
</QueueItemContent>
{todo.description && (
<QueueItemDescription completed={todo.completed}>
{todo.description}
</QueueItemDescription>
)}
</QueueItem>
))}
</QueueList>
</QueueSectionContent>
</QueueSection>
</Queue>
{/* Reasoning */}
{reasoning && (
<Reasoning isStreaming={reasoning.isStreaming}>
<ReasoningTrigger />
<ReasoningContent>{reasoning.content}</ReasoningContent>
</Reasoning>
)}
{/* Tools */}
{tools.map(tool => (
<div key={tool.id}>
<Tool>
<ToolHeader
title={tool.name}
type={tool.type}
state={tool.state}
/>
<ToolContent>
<ToolInput input={tool.args} />
<ToolOutput output={tool.result} errorText={tool.error} />
</ToolContent>
</Tool>
{tool.requiresApproval && (
<Confirmation approval={tool.approval} state={tool.state}>
<ConfirmationTitle>
Approve {tool.name}?
</ConfirmationTitle>
<ConfirmationRequest>
<ConfirmationActions>
<ConfirmationAction
onClick={() => approveTool(tool.id)}
variant="default"
>
Approve
</ConfirmationAction>
<ConfirmationAction
onClick={() => rejectTool(tool.id)}
variant="outline"
>
Reject
</ConfirmationAction>
</ConfirmationActions>
</ConfirmationRequest>
<ConfirmationAccepted>
Tool approved and executed.
</ConfirmationAccepted>
<ConfirmationRejected>
Tool execution rejected.
</ConfirmationRejected>
</Confirmation>
)}
</div>
))}
</div>
);
}