"use client";
import {
Conversation,
ConversationContent,
} from "@ai-elements/conversation";
import {
Message,
MessageContent,
} from "@ai-elements/message";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { GlobeIcon } from "lucide-react";
import React from "react";
import {
PromptInput,
PromptInputActionAddAttachments,
PromptInputActionMenu,
PromptInputActionMenuContent,
PromptInputActionMenuTrigger,
PromptInputAttachment,
PromptInputAttachments,
PromptInputBody,
PromptInputButton,
PromptInputFooter,
PromptInputSpeechButton,
PromptInputSubmit,
PromptInputTextarea,
PromptInputTools,
} from "@/components/ui/prompt-input";
import {
Sidekick,
SidekickContent,
SidekickFooter,
SidekickHeader,
SidekickInset,
SidekickProvider,
SidekickTrigger,
} from "@/components/ui/sidekick";
export function SidekickDemo() {
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
// Example: Custom implementation
const [messages, setMessages] = React.useState([
{ id: "1", from: "user", content: "Hello, how can you help me?" },
{
id: "2",
from: "assistant",
content:
"I am your AI Sidekick. I can help you with coding, writing, and more.",
},
]);
const handleCustomSubmit = (message: { text: string }) => {
// Add user message
setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
from: "user",
content: message.text,
},
]);
// Simulate assistant response (replace with your own logic)
setTimeout(() => {
setMessages((prev) => [
...prev,
{
id: (Date.now() + 1).toString(),
from: "assistant",
content: `You said: ${message.text}`,
},
]);
}, 1000);
};
return (
<div className="flex h-full w-full overflow-hidden rounded-xl border bg-background shadow-lg">
<SidekickProvider
className="min-h-0"
defaultOpen={true}
style={
{
"--sidekick-width": "320px",
} as React.CSSProperties
}
>
<SidekickInset>
<div className="flex h-full flex-col items-center justify-center bg-muted/20 p-6 text-center">
<SidekickTrigger className="mb-4 md:hidden" />
<div className="max-w-xs space-y-2">
<h3 className="font-semibold text-lg">Main Content</h3>
<p className="text-muted-foreground text-sm">
Use <kbd className="font-mono text-xs">⌘I</kbd> to toggle panel.
</p>
</div>
</div>
</SidekickInset>
<Sidekick>
<SidekickHeader>
<span className="font-semibold">AI Assistant</span>
<div className="ml-auto">
<SidekickTrigger />
</div>
</SidekickHeader>
<SidekickContent>
<Conversation>
<ConversationContent>
{messages.map((msg) => (
<Message
className={
msg.from === "user" ? "flex-row-reverse" : "flex-row"
}
from={msg.from as "user" | "assistant"}
key={msg.id}
>
<Avatar className="h-6 w-6 rounded-full">
<AvatarImage
alt={msg.from === "user" ? "@montekkundan" : "shadcn"}
src={
msg.from === "user"
? "https://github.com/montekkundan.png"
: "https://github.com/shadcn.png"
}
/>
<AvatarFallback>
{msg.from === "user" ? "MK" : "CN"}
</AvatarFallback>
</Avatar>
<MessageContent>{msg.content}</MessageContent>
</Message>
))}
</ConversationContent>
</Conversation>
</SidekickContent>
<SidekickFooter>
<PromptInput className="w-full" onSubmit={handleCustomSubmit}>
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
<PromptInputBody>
<PromptInputTextarea
placeholder="Ask me anything..."
ref={textareaRef}
/>
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger />
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
<PromptInputSpeechButton textareaRef={textareaRef} />
<PromptInputButton>
<GlobeIcon size={16} />
<span>Search</span>
</PromptInputButton>
</PromptInputTools>
<PromptInputSubmit />
</PromptInputFooter>
</PromptInput>
</SidekickFooter>
</Sidekick>
</SidekickProvider>
</div>
);
}
Overview
Sidekick is a composable chat layout system. It ships as a sidebar with optional toggle behavior, a conversation container with auto-scroll, and a set of message primitives you can style or extend.
Use it standalone, or wrap it in SidekickProvider to enable keyboard shortcuts (⌘I / Ctrl+I), responsive mobile behavior, and persisted open/closed state.
Sidekick composes with AI SDK Elements for the chat primitives:
- Conversation: https://ai-sdk.dev/elements/components/conversation
- Message: https://ai-sdk.dev/elements/components/message
Installation
bunx --bun shadcn@latest add https://sidekick.montek.dev/r/sidekick.jsonUsage
import {
Sidekick,
SidekickContent,
SidekickFooter,
SidekickHeader,
SidekickInset,
SidekickProvider,
SidekickTrigger,
} from "@/components/ui/sidekick";
import {
Conversation,
ConversationContent,
ConversationEmptyState,
ConversationScrollButton,
} from "@ai-elements/conversation";
import {
Message,
MessageActions,
MessageAvatar,
MessageContent,
MessageTimestamp,
} from "@ai-elements/message";Alternatively, use the compound component pattern:
import { Sidekick } from "@/components/ui/sidekick";
// Access subcomponents via Sidekick.Header, Sidekick.Content, etc.Provider + Toggleable Panel
Use SidekickProvider to enable toggling, keyboard shortcuts, and responsive layouts.
<SidekickProvider defaultOpen>
<SidekickInset>
{/* Main content */}
<SidekickTrigger />
</SidekickInset>
<Sidekick>
<SidekickHeader className="justify-between">
<span className="font-semibold">AI Assistant</span>
<SidekickTrigger />
</SidekickHeader>
<SidekickContent>
<Conversation>
<ConversationContent>
<ConversationEmptyState />
</ConversationContent>
<ConversationScrollButton />
</Conversation>
</SidekickContent>
<SidekickFooter>{/* Add PromptInput here */}</SidekickFooter>
</Sidekick>
</SidekickProvider>Standalone Mode
If you render Sidekick without a provider, it behaves as a fixed panel (no toggle trigger).
<Sidekick>
<SidekickHeader>
<span className="font-semibold">AI Assistant</span>
</SidekickHeader>
<SidekickContent>
<Conversation>
<ConversationContent>
<Message from="user">
<MessageContent from="user">Hello!</MessageContent>
</Message>
<Message from="assistant">
<MessageContent from="assistant">Hi there!</MessageContent>
</Message>
</ConversationContent>
</Conversation>
</SidekickContent>
<SidekickFooter>{/* Add PromptInput here */}</SidekickFooter>
</Sidekick>If you want to force standalone mode even when a provider is present, pass standalone.
Mobile Behavior
Sidekick supports three mobile behaviors:
sheet(default): full-screen sheet on mobileinline: collapsible section within the layoutfloating: off-canvas panel that slides in
<SidekickProvider defaultOpenMobile>
<Sidekick mobileBehavior="floating">...</Sidekick>
</SidekickProvider>Composition Patterns
Prompt Input Toolbars
Compose any PromptInput layout inside SidekickFooter—stack headers, chips, and inline buttons.
<SidekickFooter>
<PromptInput variant="outline" size="lg" onSubmit={handleSubmit}>
<PromptInput.Header>
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
<span className="whitespace-nowrap">Tone: Confident</span>
<span className="whitespace-nowrap">Audience: Execs</span>
</div>
</PromptInput.Header>
<PromptInput.Body>
<PromptInput.Textarea minRows={4} placeholder="Draft the launch recap..." />
<PromptInput.Button>
<MicIcon className="size-4" />
</PromptInput.Button>
</PromptInput.Body>
<PromptInput.Footer className="flex-col gap-3">
<PromptInput.Tools className="gap-2">
<PromptInput.Button className="gap-1 px-3">
<SparklesIcon className="size-4" />
<span>Rewrite</span>
</PromptInput.Button>
<PromptInput.Button className="gap-1 px-3">
<GlobeIcon className="size-4" />
<span>Search Docs</span>
</PromptInput.Button>
</PromptInput.Tools>
<PromptInput.Submit status={status} />
</PromptInput.Footer>
</PromptInput>
</SidekickFooter>Add PromptInputProvider around Sidekick when presets, external buttons, or docking menus need the same input state.
Persisting Conversations
Conversation renders whatever list you feed it. Store messages with Drizzle/Prisma and hydrate them when the sidebar mounts.
const messages = await db.query.messages.findMany({
where: eq(messages.threadId, params.threadId),
orderBy: asc(messages.createdAt),
})
return (
<Conversation>
<ConversationContent>
{messages.map((msg) => (
<Message className="group" from={msg.from} key={msg.id}>
<MessageAvatar />
<div className="space-y-1">
<MessageContent from={msg.from}>{msg.body}</MessageContent>
<div className="flex items-center gap-2">
<MessageTimestamp>{formatTime(msg.createdAt)}</MessageTimestamp>
<MessageActions>
{/* Copy / react / pin */}
</MessageActions>
</div>
</div>
</Message>
))}
</ConversationContent>
</Conversation>
)Features
- Collapsible Panel: Toggle the sidebar with keyboard shortcut (
⌘I/Ctrl+I) - Mobile Modes:
sheet,inline, orfloating - Conversation History: Auto-scrolling message list with scroll-to-bottom button
- Message Bubbles: Styled user/assistant bubbles with optional avatars
- Empty State: Customizable empty state when no messages exist
- Keyboard Navigation: Full keyboard accessibility
Components
SidekickProvider
Wraps your app and provides the sidebar state context.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | true | Initial open state |
defaultOpenMobile | boolean | false | Initial mobile open state |
open | boolean | - | Controlled open state |
onOpenChange | (open: boolean) => void | - | Callback when open state changes |
Sidekick
The sidebar panel component.
| Prop | Type | Default | Description |
|---|---|---|---|
side | "left" | "right" | "right" | Which side the panel appears on |
mobileBehavior | "sheet" | "inline" | "floating" | "sheet" | Mobile rendering mode |
standalone | boolean | false | When true, renders as a fixed panel without toggle behavior |
SidekickTrigger
A button to toggle the sidebar open/closed. Returns null when no provider is present.
SidekickHeader
The header section of the sidebar.
SidekickContent
The main content area of the sidebar (typically contains Conversation).
SidekickFooter
The footer section (typically contains PromptInput).
SidekickInset
The main content area that sits beside the sidebar.
SidekickSeparator
A styled separator for toolbars and lists.
SidekickInput
A compact input field for filters or search inside the sidebar.
Conversation
Container for the message list with auto-scroll functionality.
ConversationContent
The scrollable content area inside Conversation.
ConversationScrollButton
A floating scroll-to-bottom button that appears when the user is not at the end.
ConversationEmptyState
Displayed when there are no messages.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | "No messages yet" | Title text |
description | string | - | Description text |
icon | ReactNode | - | Icon to display |
Message
Container for a single message.
| Prop | Type | Default | Description |
|---|---|---|---|
from | "user" | "assistant" | "assistant" | Who sent the message |
MessageContent
The message bubble with text content.
| Prop | Type | Default | Description |
|---|---|---|---|
from | "user" | "assistant" | "assistant" | Styles the bubble based on sender |
MessageAvatar
Container for the sender's avatar.
MessageTimestamp
Text-only timestamp styling.
MessageActions
An actions row that fades in on hover. Add className="group" to the parent Message to enable hover behavior.
Hooks
useSidekick
Access the sidebar state and controls.
const { state, open, setOpen, toggleSidekick } = useSidekick();useConversation
Access conversation scroll controls.
const { scrollToBottom, isAtBottom } = useConversation();Theming
Sidekick supports theming through CSS classes. Apply a theme by adding the theme class to your provider:
Custom Themes
Create your own theme by targeting the data attributes:
.my-theme [data-slot="sidekick"] {
background-color: #1a1a1a;
}
.my-theme [data-slot="message-content"][data-from="user"] {
background-color: #0078d4;
}
.my-theme [data-slot="message-content"][data-from="assistant"] {
background-color: #2d2d2d;
}On This Page
OverviewInstallationUsageProvider + Toggleable PanelStandalone ModeMobile BehaviorComposition PatternsPrompt Input ToolbarsPersisting ConversationsFeaturesComponentsSidekickProviderSidekickSidekickTriggerSidekickHeaderSidekickContentSidekickFooterSidekickInsetSidekickSeparatorSidekickInputConversationConversationContentConversationScrollButtonConversationEmptyStateMessageMessageContentMessageAvatarMessageTimestampMessageActionsHooksuseSidekickuseConversationThemingCustom Themes