A group of interactive buttons with optional tooltips, commonly used for message-level interactions like copy, like, share, etc.
actions-default
"use client"
import { useState } from "react"
import {
BookmarkIcon,
CopyIcon,
RefreshCcwIcon,
Share2Icon,
ThumbsDownIcon,
ThumbsUpIcon,
} from "lucide-react"
import { Action, Actions } from "@/components/ui/ai/actions"
export function ActionsDemo() {
const [liked, setLiked] = useState<null | "up" | "down">(null)
const [saved, setSaved] = useState(false)
return (
<div className="flex h-full items-center justify-center">
<div className="mx-auto max-w-md">
<Actions className="flex flex-wrap gap-2">
{/* Like / Dislike Toggle */}
<Action
label={liked === "up" ? "Unlike" : "Like"}
onClick={() => setLiked(liked === "up" ? null : "up")}
className={liked === "up" ? "bg-green-100 text-green-600" : ""}
>
<ThumbsUpIcon className="size-4" />
</Action>
<Action
label={liked === "down" ? "Undo dislike" : "Dislike"}
onClick={() => setLiked(liked === "down" ? null : "down")}
className={liked === "down" ? "bg-red-100 text-red-600" : ""}
>
<ThumbsDownIcon className="size-4" />
</Action>
{/* Retry */}
<Action label="Retry" onClick={() => alert("Retrying...")}>
<RefreshCcwIcon className="size-4" />
</Action>
{/* Copy */}
<Action
label="Copy"
onClick={() => navigator.clipboard.writeText("Copied content")}
>
<CopyIcon className="size-4" />
</Action>
{/* Share */}
<Action
label="Share"
onClick={() =>
navigator.share
? navigator.share({ text: "Shared content" })
: alert("Sharing not supported")
}
>
<Share2Icon className="size-4" />
</Action>
{/* Save Toggle */}
<Action
label={saved ? "Unsave" : "Save"}
onClick={() => setSaved((prev) => !prev)}
className={saved ? "bg-blue-100 text-blue-600" : ""}
>
<BookmarkIcon className="size-4" />
</Action>
</Actions>
</div>
</div>
)
}
pnpm dlx shadcn@latest add "https://ui.dalim.in/r/actions-default.json"
actions-hover
This is a response from an assistant.
Try hovering over this message to see the actions appear!
"use client"
import { useState } from "react"
import {
CopyIcon,
HeartIcon,
RefreshCcwIcon,
ShareIcon,
ThumbsDownIcon,
ThumbsUpIcon,
} from "lucide-react"
import { Action, Actions } from "@/components/ui/ai/actions"
import { Message, MessageContent } from "@/components/ui/ai/message"
const Example = () => {
const [liked, setLiked] = useState(false)
const [disliked, setDisliked] = useState(false)
const [favorited, setFavorited] = useState(false)
const responseContent = `This is a response from an assistant.
Try hovering over this message to see the actions appear!`
const handleRetry = () => {
console.log("Retrying request...")
}
const handleCopy = (content?: string) => {
console.log("Copied:", content)
}
const handleShare = (content?: string) => {
console.log("Sharing:", content)
}
const actions = [
{
icon: RefreshCcwIcon,
label: "Retry",
onClick: handleRetry,
},
{
icon: ThumbsUpIcon,
label: "Like",
onClick: () => setLiked(!liked),
},
{
icon: ThumbsDownIcon,
label: "Dislike",
onClick: () => setDisliked(!disliked),
},
{
icon: CopyIcon,
label: "Copy",
onClick: () => handleCopy(responseContent),
},
{
icon: ShareIcon,
label: "Share",
onClick: () => handleShare(responseContent),
},
{
icon: HeartIcon,
label: "Favorite",
onClick: () => setFavorited(!favorited),
},
]
return (
<div className="flex h-full items-center justify-center">
<Message
className="group flex flex-col items-start gap-2"
from="assistant"
>
<MessageContent>{responseContent}</MessageContent>
<Actions className="mt-2 opacity-0 group-hover:opacity-100">
{actions.map((action) => (
<Action key={action.label} label={action.label}>
<action.icon className="size-4" />
</Action>
))}
</Actions>
</Message>
</div>
)
}
export Example
pnpm dlx shadcn@latest add "https://ui.dalim.in/r/actions-hover.json"
Installation
pnpm dlx dlx shadcn@latest add https://ui.dalim.in/r/styles/default/actions.json
Usage
import {
Action,
Actions,
} from "@/components/ui/ai/actions"
import { CopyIcon, ShareIcon } from "lucide-react"
<Actions>
<Action tooltip="Copy" label="Copy">
<CopyIcon className="size-4" />
</Action>
<Action tooltip="Share" label="Share">
<ShareIcon className="size-4" />
</Action>
</Actions>
Usage with AI SDK
Build a simple chat UI where the user can copy or regenerate the most recent message.
Add the following component to your frontend:
'use client';
import { useState } from 'react';
import { Actions, Action } from '@/components/ui/ai/actions';
import { Message, MessageContent } from '@/components/ui/ai/message';
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from '@/components/ui/ai/conversation';
import {
Input,
PromptInputTextarea,
PromptInputSubmit,
} from '@/components/ui/ai/prompt-input';
import { Response } from '@/components/ui/ai/response';
import { RefreshCcwIcon, CopyIcon } from 'lucide-react';
import { useChat } from '@ai-sdk/react';
import { Fragment } from 'react';
const ActionsDemo = () => {
const [input, setInput] = useState('');
const { messages, sendMessage, status, regenerate } = useChat();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (input.trim()) {
sendMessage({ text: input });
setInput('');
}
};
return (
<div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
<div className="flex flex-col h-full">
<Conversation>
<ConversationContent>
{messages.map((message, messageIndex) => (
<Fragment key={message.id}>
{message.parts.map((part, i) => {
switch (part.type) {
case 'text':
const isLastMessage =
messageIndex === messages.length - 1;
return (
<Fragment key={`${message.id}-${i}`}>
<Message from={message.role}>
<MessageContent>
<Response>{part.text}</Response>
</MessageContent>
</Message>
{message.role === 'assistant' && isLastMessage && (
<Actions>
<Action
onClick={() => regenerate()}
label="Retry"
>
<RefreshCcwIcon className="size-3" />
</Action>
<Action
onClick={() =>
navigator.clipboard.writeText(part.text)
}
label="Copy"
>
<CopyIcon className="size-3" />
</Action>
</Actions>
)}
</Fragment>
);
default:
return null;
}
})}
</Fragment>
))}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
<Input
onSubmit={handleSubmit}
className="mt-4 w-full max-w-2xl mx-auto relative"
>
<PromptInputTextarea
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
className="pr-12"
/>
<PromptInputSubmit
status={status === 'streaming' ? 'streaming' : 'ready'}
disabled={!input.trim()}
className="absolute bottom-1 right-1"
/>
</Input>
</div>
</div>
);
};
export default ActionsDemo;
Add the following route to your backend:
import { streamText, UIMessage, convertToModelMessages } from 'ai';
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { model, messages }: { messages: UIMessage[]; model: string } =
await req.json();
const result = streamText({
model: 'openai/gpt-4o',
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}
Features
- Row of composable action buttons with consistent styling
- Support for custom actions with tooltips
- State management for toggle actions (like, dislike, favorite)
- Keyboard accessible with proper ARIA labels
- Clipboard and Web Share API integration
- TypeScript support with proper type definitions
- Consistent with design system styling
API Reference
Actions
Container for a group of action buttons.
Prop | Type | Default | Description |
---|---|---|---|
className | string | – | Additional classes to style the wrapper. |
children | ReactNode | – | One or more <Action /> components. |
Action
An individual interactive button with optional tooltip. Inherits all props from the underlying Button
.
Prop | Type | Default | Description |
---|---|---|---|
tooltip | string | – | Text shown in a tooltip when hovering the button. |
label | string | – | Accessible label (also rendered as sr-only text). |
variant | string | "ghost" | Button variant (e.g., "ghost" , "default" , "outline" ). |
size | string | "sm" | Button size (e.g., "sm" , "md" , "lg" ). |
className | string | – | Additional classes for styling the button. |
children | ReactNode | – | The icon or content inside the action button. |
...props | Button props | – | Any additional props supported by the Button component. |