Skip to main content
The Vercel AI Chatbot uses a modular component architecture built with React, shadcn/ui, and Tailwind CSS. You can customize existing components or create new ones to match your application’s needs.

Component structure

Components are organized in three main directories:
  • components/ui/ - Base shadcn/ui components (Button, Card, Input, etc.)
  • components/ai-elements/ - AI-specific components (Message, Conversation, etc.)
  • components/ - Application-level components (Chat, Sidebar, etc.)

Message components

Message components form the core of the chat interface.

Basic message structure

components/ai-elements/message.tsx
import { cn } from "@/lib/utils";

export type MessageProps = HTMLAttributes<HTMLDivElement> & {
  from: UIMessage["role"];
};

export const Message = ({ className, from, ...props }: MessageProps) => (
  <div
    className={cn(
      "group flex w-full max-w-[95%] flex-col gap-2",
      from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
      className
    )}
    {...props}
  />
);

Message content

Message content is styled differently based on the sender:
components/ai-elements/message.tsx
export const MessageContent = ({ children, className, ...props }) => (
  <div
    className={cn(
      "is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2",
      "group-[.is-user]:ml-auto group-[.is-user]:rounded-lg",
      "group-[.is-user]:bg-secondary group-[.is-user]:px-4",
      "group-[.is-user]:py-3 group-[.is-user]:text-foreground",
      "group-[.is-assistant]:text-foreground",
      className
    )}
    {...props}
  >
    {children}
  </div>
);
User messages have a secondary background and are right-aligned, while assistant messages have no background and are left-aligned. Customize these styles by modifying the className utilities.

Message actions

Action buttons for copying, editing, or regenerating messages:
components/ai-elements/message.tsx
export type MessageActionProps = ComponentProps<typeof Button> & {
  tooltip?: string;
  label?: string;
};

export const MessageAction = ({
  tooltip,
  children,
  label,
  variant = "ghost",
  size = "icon-sm",
  ...props
}: MessageActionProps) => {
  const button = (
    <Button size={size} type="button" variant={variant} {...props}>
      {children}
      <span className="sr-only">{label || tooltip}</span>
    </Button>
  );

  if (tooltip) {
    return (
      <Tooltip>
        <TooltipTrigger asChild>{button}</TooltipTrigger>
        <TooltipContent>
          <p>{tooltip}</p>
        </TooltipContent>
      </Tooltip>
    );
  }

  return button;
};

Customizing message appearance

1

Create custom message wrapper

Extend the base Message component with custom styling:
components/custom-message.tsx
import { Message, MessageContent } from "@/components/ai-elements/message";

export function CustomMessage({ from, children, ...props }) {
  return (
    <Message from={from} {...props}>
      <MessageContent className="group-[.is-user]:bg-blue-500 group-[.is-user]:text-white">
        {children}
      </MessageContent>
    </Message>
  );
}
2

Add custom message metadata

Display timestamps, user avatars, or other metadata:
components/custom-message.tsx
export function MessageWithTimestamp({ from, timestamp, children }) {
  return (
    <Message from={from}>
      <div className="flex items-center gap-2 text-xs text-muted-foreground">
        <Avatar src={from === "user" ? userAvatar : assistantAvatar} />
        <time>{new Date(timestamp).toLocaleTimeString()}</time>
      </div>
      <MessageContent>{children}</MessageContent>
    </Message>
  );
}
3

Use in chat interface

Replace the default message component in your chat interface:
components/chat.tsx
import { CustomMessage } from "@/components/custom-message";

export function Chat({ messages }) {
  return (
    <div className="flex flex-col gap-4">
      {messages.map((message) => (
        <CustomMessage
          key={message.id}
          from={message.role}
          timestamp={message.createdAt}
        >
          {message.content}
        </CustomMessage>
      ))}
    </div>
  );
}

Button components

Buttons use class-variance-authority (CVA) for type-safe variant management:
components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority";

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md...",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline: "border border-input bg-background hover:bg-accent",
        secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
        "icon-sm": "h-8 w-8",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

Adding custom button variants

components/ui/button.tsx
const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2...",
  {
    variants: {
      variant: {
        // ... existing variants
        success: "bg-green-600 text-white hover:bg-green-700",
        warning: "bg-yellow-600 text-white hover:bg-yellow-700",
      },
      size: {
        // ... existing sizes
        xs: "h-6 px-2 text-xs",
      },
    },
  }
);
Use custom variants:
<Button variant="success" size="xs">Save</Button>
<Button variant="warning" size="sm">Warning</Button>

Card components

Cards are used for displaying structured content:
components/ui/card.tsx
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div
      className={cn(
        "rounded-lg border bg-card text-card-foreground shadow-sm",
        className
      )}
      ref={ref}
      {...props}
    />
  )
);

const CardHeader = ({ className, ...props }) => (
  <div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
);

const CardTitle = ({ className, ...props }) => (
  <div
    className={cn("font-semibold text-2xl leading-none tracking-tight", className)}
    {...props}
  />
);

const CardContent = ({ className, ...props }) => (
  <div className={cn("p-6 pt-0", className)} {...props} />
);

Using cards

<Card>
  <CardHeader>
    <CardTitle>Weather in San Francisco</CardTitle>
    <CardDescription>Current conditions</CardDescription>
  </CardHeader>
  <CardContent>
    <p>Temperature: 72°F</p>
    <p>Conditions: Sunny</p>
  </CardContent>
  <CardFooter>
    <Button>Refresh</Button>
  </CardFooter>
</Card>

Message attachments

Display file attachments in messages:
components/ai-elements/message.tsx
export function MessageAttachment({ data, className, onRemove }) {
  const filename = data.filename || "";
  const mediaType = data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
  const isImage = mediaType === "image";

  return (
    <div className={cn("group relative size-24 overflow-hidden rounded-lg", className)}>
      {isImage ? (
        <>
          <img
            alt={filename || "attachment"}
            className="size-full object-cover"
            src={data.url}
          />
          {onRemove && (
            <Button
              className="absolute top-2 right-2 size-6 rounded-full opacity-0 group-hover:opacity-100"
              onClick={(e) => {
                e.stopPropagation();
                onRemove();
              }}
              variant="ghost"
            >
              <XIcon />
            </Button>
          )}
        </>
      ) : (
        <div className="flex size-full items-center justify-center bg-muted">
          <PaperclipIcon className="size-4" />
        </div>
      )}
    </div>
  );
}

Branch navigation

Allow users to navigate between message variations:
components/ai-elements/message.tsx
export const MessageBranch = ({ defaultBranch = 0, onBranchChange, ...props }) => {
  const [currentBranch, setCurrentBranch] = useState(defaultBranch);
  const [branches, setBranches] = useState<ReactElement[]>([]);

  const goToPrevious = () => {
    const newBranch = currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
    setCurrentBranch(newBranch);
    onBranchChange?.(newBranch);
  };

  const goToNext = () => {
    const newBranch = currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
    setCurrentBranch(newBranch);
    onBranchChange?.(newBranch);
  };

  return (
    <MessageBranchContext.Provider value={{ currentBranch, goToPrevious, goToNext }}>
      <div {...props} />
    </MessageBranchContext.Provider>
  );
};
Usage:
<MessageBranch>
  <MessageBranchContent>
    <MessageResponse>First variation</MessageResponse>
    <MessageResponse>Second variation</MessageResponse>
    <MessageResponse>Third variation</MessageResponse>
  </MessageBranchContent>
  <MessageBranchSelector from="assistant">
    <MessageBranchPrevious />
    <MessageBranchPage />
    <MessageBranchNext />
  </MessageBranchSelector>
</MessageBranch>

Creating custom tool UI

Create custom components to display tool results:
components/weather.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

export function WeatherCard({ data }) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>
          Weather in {data.cityName || "Unknown Location"}
        </CardTitle>
      </CardHeader>
      <CardContent>
        <div className="flex items-center gap-4">
          <div className="text-4xl font-bold">
            {data.current.temperature_2m}°C
          </div>
          <div className="text-muted-foreground">
            <p>Sunrise: {data.daily.sunrise[0]}</p>
            <p>Sunset: {data.daily.sunset[0]}</p>
          </div>
        </div>
      </CardContent>
    </Card>
  );
}
Register custom UI in your chat interface:
components/chat.tsx
function renderToolResult(toolName: string, result: any) {
  switch (toolName) {
    case "getWeather":
      return <WeatherCard data={result} />;
    case "getStockPrice":
      return <StockPriceCard data={result} />;
    default:
      return <pre>{JSON.stringify(result, null, 2)}</pre>;
  }
}

Markdown rendering

Messages support markdown rendering with Streamdown:
components/ai-elements/message.tsx
import { Streamdown } from "streamdown";

export const MessageResponse = memo(
  ({ className, ...props }: MessageResponseProps) => (
    <Streamdown
      className={cn(
        "size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
        className
      )}
      {...props}
    />
  ),
  (prevProps, nextProps) => prevProps.children === nextProps.children
);
Streamdown automatically renders:
  • Code blocks with syntax highlighting
  • Tables
  • Lists
  • Links
  • Emphasis and strong text
  • Math equations (KaTeX)

Accessibility features

Screen reader support: Use <span className="sr-only"> for icon-only buttons to provide context for screen readers.
<Button variant="ghost" size="icon-sm">
  <TrashIcon />
  <span className="sr-only">Delete message</span>
</Button>
Keyboard navigation: All interactive components support keyboard navigation. Focus states are styled with focus-visible:ring-2.
ARIA labels: Use aria-label for dynamic or complex UI elements:
<button aria-label="Next branch" onClick={goToNext}>
  <ChevronRightIcon />
</button>

Component composition patterns

Slot pattern for flexible composition

components/ui/button.tsx
import { Slot as SlotPrimitive } from "radix-ui";

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? SlotPrimitive.Slot : "button";
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    );
  }
);
Use asChild to render a button as a link:
<Button asChild>
  <Link href="/chat/new">New Chat</Link>
</Button>

Context-based component communication

The message branch system uses context for state management:
const MessageBranchContext = createContext<MessageBranchContextType | null>(null);

const useMessageBranch = () => {
  const context = useContext(MessageBranchContext);
  if (!context) {
    throw new Error("MessageBranch components must be used within MessageBranch");
  }
  return context;
};
Use composition patterns to build flexible, reusable components that can be easily customized without modifying the source code.

Build docs developers (and LLMs) love