Elements

Elements · Primitives

Tooltip icon button

An accessible icon button with a tooltip label and shared interaction states.

fig. 01

Installation

npx shadcn@latest add "@assistant-ui/tooltip-icon-button"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

TooltipIconButton wraps a single icon in a button that always carries an accessible name and a hover tooltip, both driven by the same tooltip string. With a runtime it's the button most kit elements reach for whenever a primitive needs a trigger; standalone you drive it with your own onClick and disabled.

Getting started

Every action button across the runtime-connected kit elements, thread, voice, attachment, the assistant modal, is a TooltipIconButton, usually composed with a primitive through render so the primitive owns the click handling and the disabled or hidden state.

Compose it with a primitive

components/assistant-ui/elements/thread.aui.tsx
"use client";

import { ThreadPrimitive } from "@assistant-ui/react";
import { ArrowDownIcon } from "lucide-react";
import { TooltipIconButton } from "@/components/assistant-ui/elements/tooltip-icon-button";

function ScrollToBottom() {
  return (
    <ThreadPrimitive.ScrollToBottom
      render={<TooltipIconButton tooltip="Scroll to bottom" variant="outline" />}
    >
      <ArrowDownIcon />
    </ThreadPrimitive.ScrollToBottom>
  );
}

The primitive supplies the click handler and hides itself, ScrollToBottom in particular renders nothing once the thread is already at the bottom; TooltipIconButton only supplies the look, the tooltip, and the accessible name.

Install a kit element for it pre-wired

@assistant-ui/thread alone wires up scroll-to-bottom, send, voice dictation start and stop, copy, refresh, edit, and branch previous and next, all as TooltipIconButtons. @assistant-ui/voice, @assistant-ui/attachment, and @assistant-ui/assistant-modal each add a few more of their own. None of that composition lives in this file, so you rarely reach for TooltipIconButton directly once those are installed.

Anatomy

<button data-slot="button">
  {/* the icon you pass as children */}
  <span className="sr-only">{/* tooltip text, as the accessible name */}</span>
</button>

A tooltip provider and a floating content panel wrap the button but render nothing of their own until it's hovered or focused. tooltip does double duty: it's both the floating label and, through the sr-only span, the button's only accessible name, there's no separate aria-label prop. side positions the floating tooltip and defaults to "bottom".

Examples

Every prop Button takes, still works

TooltipIconButtonProps extends Button's own props, so variant ("default" | "outline" | "secondary" | "ghost" | "destructive" | "link"), size, and disabled all pass straight through. The component itself only fixes variant="ghost" and size="icon" as defaults, both overridable the normal way:

<TooltipIconButton tooltip="Delete" variant="destructive">
  <TrashIcon />
</TooltipIconButton>

Restyle the button

className merges onto the rendered Button, on top of this file's own size-6 p-1 default. It does not read from surfaces.tsx, unlike most of the catalog, its look comes from the shadcn-style Button and tooltip primitives in components/ui.

<TooltipIconButton tooltip="Copy" className="size-8">
  <CopyIcon />
</TooltipIconButton>

API reference

Composition sites

FileTooltip text
thread.aui.tsx"Scroll to bottom", "Send message", "Voice input", "Stop dictation"
thread.aui.tsx"Copy", "Refresh", "More", "Edit", "Previous", "Next"
voice.aui.tsx"Mute" / "Unmute", "Disconnect"
attachment.aui.tsx"Add Attachment", "Remove file"
assistant-modal.aui.tsx"Open Assistant" / "Close Assistant"
markdown-text.tsx"Copy", on a rendered code block

There's no state selector of its own: it renders whatever the primitive it wraps decides, through render.