Chatbot

Actions

A row of composable action buttons for AI responses, including retry, like, dislike, copy, share, and custom actions.

The Actions component provides a flexible row of action buttons for AI responses with common actions like retry, like, dislike, copy, and share.

Hello, how are you?
I am fine, thank you!
"use client";import { Action, Actions } from "@/components/ai-elements/elements/actions";import { Conversation, ConversationContent } from "@/components/ai-elements/elements/conversation";import { Message, MessageContent } from "@/components/ai-elements/elements/message";import {  CopyIcon,  RefreshCcwIcon,  ShareIcon,  ThumbsDownIcon,  ThumbsUpIcon,} from "lucide-react";import { nanoid } from "nanoid";import { useState } from "react";const messages: {  key: string;  from: "user" | "assistant";  content: string;  avatar: string;  name: string;}[] = [  {    key: nanoid(),    from: "user",    content: "Hello, how are you?",    avatar: "https://github.com/haydenbleasel.png",    name: "Hayden Bleasel",  },  {    key: nanoid(),    from: "assistant",    content: "I am fine, thank you!",    avatar: "https://github.com/openai.png",    name: "OpenAI",  },];const Example = () => {  const [liked, setLiked] = useState(false);  const [disliked, setDisliked] = useState(false);  const [favorited, setFavorited] = useState(false);  const handleRetry = () => {};  const handleCopy = () => {};  const handleShare = () => {};  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(),    },    {      icon: ShareIcon,      label: "Share",      onClick: () => handleShare(),    },  ];  return (    <Conversation className="relative w-full">      <ConversationContent>        {messages.map((message) => (          <Message            className={`flex flex-col gap-2 ${message.from === "assistant" ? "items-start" : "items-end"}`}            from={message.from}            key={message.key}          >            <MessageContent>{message.content}</MessageContent>            {message.from === "assistant" && (              <Actions className="mt-2">                {actions.map((action) => (                  <Action key={action.label} label={action.label}>                    <action.icon className="size-4" />                  </Action>                ))}              </Actions>            )}          </Message>        ))}      </ConversationContent>    </Conversation>  );};export default Example;

Installation

npx ai-elements@latest add actions
npx shadcn@latest add @ai-elements/actions
"use client";import { Button } from "@repo/shadcn-ui/components/ui/button";import {  Tooltip,  TooltipContent,  TooltipProvider,  TooltipTrigger,} from "@repo/shadcn-ui/components/ui/tooltip";import { cn } from "@repo/shadcn-ui/lib/utils";import type { ComponentProps } from "react";export type ActionsProps = ComponentProps<"div">;export const Actions = ({ className, children, ...props }: ActionsProps) => (  <div className={cn("flex items-center gap-1", className)} {...props}>    {children}  </div>);export type ActionProps = ComponentProps<typeof Button> & {  tooltip?: string;  label?: string;};export const Action = ({  tooltip,  children,  label,  className,  variant = "ghost",  size = "sm",  ...props}: ActionProps) => {  const button = (    <Button      className={cn(        "relative size-9 p-1.5 text-muted-foreground hover:text-foreground",        className      )}      size={size}      type="button"      variant={variant}      {...props}    >      {children}      <span className="sr-only">{label || tooltip}</span>    </Button>  );  if (tooltip) {    return (      <TooltipProvider>        <Tooltip>          <TooltipTrigger asChild>{button}</TooltipTrigger>          <TooltipContent>            <p>{tooltip}</p>          </TooltipContent>        </Tooltip>      </TooltipProvider>    );  }  return button;};

Usage

import { Actions, Action } from '@/components/ai-elements/actions';
import { ThumbsUpIcon } from 'lucide-react';
<Actions className="mt-2">
  <Action label="Like">
    <ThumbsUpIcon 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:

app/page.tsx
'use client';

import { useState } from 'react';
import { Actions, Action } from '@/components/ai-elements/actions';
import { Message, MessageContent } from '@/components/ai-elements/message';
import {
  Conversation,
  ConversationContent,
  ConversationScrollButton,
} from '@/components/ai-elements/conversation';
import {
  Input,
  PromptInputTextarea,
  PromptInputSubmit,
} from '@/components/ai-elements/prompt-input';
import { Response } from '@/components/ai-elements/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:

api/chat/route.ts
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

Examples

This is a response from an assistant. Try hovering over this message to see the actions appear!
"use client";import { Action, Actions } from "@/components/ai-elements/elements/actions";import { Message, MessageContent } from "@/components/ai-elements/elements/message";import {  CopyIcon,  HeartIcon,  RefreshCcwIcon,  ShareIcon,  ThumbsDownIcon,  ThumbsUpIcon,} from "lucide-react";import { useState } from "react";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 (    <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-3" />          </Action>        ))}      </Actions>    </Message>  );};export default Example;

Props

<Actions />

Prop

Type

<Action />

Prop

Type