v1.5

Chat Attachment

File and media previews for AI chat, plus the picker and drop target that collect them.

Pro
create-banner.png
project-brief.pdf
805 KB
quarterly-report.xlsx
96 KB

Description

ChatAttachment shows one file attached to a prompt or a message. Images and videos with a preview URL render as a square thumbnail with the name on hover. Everything else renders as a card with a FileFormat icon, the name and the size. It is a compound component: the root renders a <div> and picks the layout, and Preview, Icon, Info, Name, Meta and Remove let you rebuild the inside. ChatAttachmentGroup wraps several tiles, and ChatAttachmentInput adds a file picker, a drop target and image paste to a composer.

Use it in a chat composer's attachment row, above a user's message bubble, or inside an assistant reply that returns a file. status="uploading" and status="error" cover the time between picking a file and your server accepting it.

It is not an upload manager. For a standalone upload form with pause, retry and per-file progress bars, use File Upload. For a plain drag and drop area outside a chat surface, use Dropzone. If you only need the format glyph, use File Format.

ChatAttachment is a Create UI Pro component. Install it with a Pro seat.

Installation

pnpm dlx @create-ui/cli add chat-attachment

Anatomy

<ChatAttachmentInput>
  <ChatAttachmentInput.Dropzone>
    <ChatAttachmentGroup>
      <ChatAttachment>
        <ChatAttachment.Preview>
          <ChatAttachment.Icon />
        </ChatAttachment.Preview>
        <ChatAttachment.Info>
          <ChatAttachment.Name />
          <ChatAttachment.Meta />
        </ChatAttachment.Info>
        <ChatAttachment.Remove />
      </ChatAttachment>
    </ChatAttachmentGroup>
    <ChatAttachmentInput.Trigger />
  </ChatAttachmentInput.Dropzone>
</ChatAttachmentInput>

Usage

import { ChatAttachment } from "@/components/ui/chat-attachment"
<ChatAttachment name="project-brief.pdf" fileSize={824120} />

Examples

Every example is a Pro preview (marked with a Pro badge).

Thumbnail or card

The layout follows the data. An image or video with src becomes a thumbnail. Without src there is nothing to preview, so the same file renders as a card. Set mediaType="file" to force the card when you have a URL but want the name visible.

Pro
team-offsite.jpg
team-offsite.jpg
313 KB
team-offsite.jpg
313 KB

Sizes

size="md" is the 64px composer tile. size="sm" shrinks it to 40px for queued prompts and dense message history. A small thumbnail drops the hover name, since it would not fit.

Pro
calendar-view.png
project-brief.pdf
805 KB
calendar-view.png
project-brief.pdf
805 KB

Upload status

status="uploading" with progress shows a progress ring and the percentage. Leave progress out for a spinner when you have no byte count. status="error" tints the tile and shows error, which defaults to "Upload failed". On a thumbnail the message is announced to screen readers only.

Pro
launch-deck.pptx
64%
team-offsite.jpg
design-assets.zip
Uploading
team-offsite.jpg
raw-footage.mov
File is larger than 20 MB
team-offsite.jpg
Upload failed

Custom content

Pass children to Icon for a brand glyph or a different FileFormat style, and to Meta to replace the size line. Preview asChild turns your own element, such as a <video> with a poster, into the preview surface.

Pro
design-system.fig
4.5 MB
meeting-notes.docx
42 KB · 3 pages

File input

ChatAttachmentInput owns a hidden file input. Trigger opens it and Dropzone accepts dropped files and pasted images. accept and maxSize apply to every source, and anything filtered out reaches onFilesRejected with a reason.

Pro

or drop PDF and CSV files here

With Prompt Input

asChild puts the drop target on PromptInput.Shell and the trigger on PromptInput.Action, so the whole composer accepts files without an extra wrapper. The shell styles its own dragging state from data-state.

Pro

Accessibility

A tile is not focusable. The only interactive parts are Remove and ChatAttachmentInput.Trigger, which are regular buttons.

KeyDescription
TabMoves focus to the next remove button or trigger.
Enter / SpaceActivates the focused remove button, or opens the file picker.
Ctrl/Cmd + VInside a Dropzone, pastes clipboard images as attachments.

ARIA notes:

  • Remove is labelled "Remove {name}" by default. Pass aria-label when the action differs, for example "Cancel upload of {name}".
  • On a thumbnail, Remove stays hidden until the tile is hovered or holds focus. Touch devices show it at reduced opacity. The hit area is 24px even though the visible chip is 16px.
  • The image alt and the video aria-label come from name. The hover name overlay repeats it, so it is aria-hidden.
  • The FileFormat icon is aria-hidden because the name is already visible.
  • Upload progress renders Progress (role="progressbar") or Spinner (role="status"), labelled "Uploading {name}". Errors are not live regions, so announce failures from your own aria-live region if the user needs to hear them right away.
  • The hidden file input is aria-hidden and out of the tab order. Give an icon-only Trigger an aria-label.

Styling

Tailwind override: pass className to any part. Classes merge with the component's CVA classes via cn().

<ChatAttachment name="notes.md" className="max-w-80" />

Data slots and attributes: the component sets these for CSS targeting:

  • data-slot="chat-attachment" on the root, with data-layout ("media" or "file"), data-media-type ("image", "video", "file"), data-size ("sm", "md") and data-status ("idle", "uploading", "error").
  • data-slot="chat-attachment-preview" on the preview surface.
  • data-slot="chat-attachment-preview-image" and data-slot="chat-attachment-preview-video" on the default thumbnail element.
  • data-slot="chat-attachment-status" on the progress or error overlay drawn over a thumbnail.
  • data-slot="chat-attachment-icon" on the icon wrapper, with data-format (for example "PDF").
  • data-slot="chat-attachment-info", data-slot="chat-attachment-name" and data-slot="chat-attachment-meta" on the text parts.
  • data-slot="chat-attachment-remove" on the remove button.
  • data-slot="chat-attachment-group" on the group.
  • data-slot="chat-attachment-input-field" on the hidden <input type="file">.
  • data-slot="chat-attachment-input-trigger" on the trigger.
  • data-slot="chat-attachment-input-dropzone" on the drop target, with data-state ("idle", "dragging", "disabled").
[data-slot="chat-attachment"][data-status="error"]
  [data-slot="chat-attachment-name"] {
  /* ... */
}

The default dropzone reserves a transparent dashed border, so the data-state="dragging" highlight never shifts layout. With asChild the target keeps its own classes and should style data-state="dragging" itself, as PromptInput.Shell does.

  • File Upload: full upload list with pause, retry and progress bars, outside a chat composer.
  • Dropzone: standalone drag and drop area with its own title, hint and button.
  • File Format: the file-type glyph on its own, for tables and file trees.
  • Prompt Input: the chat composer these tiles and the input usually sit in.

API Reference

ChatAttachment

The tile. Extends React.ComponentProps<"div">. Also exported as ChatAttachmentRoot and ChatAttachment.Root. Without children it renders Preview and Info.

Props

PropTypeDefaultDescription
namestring-File name. Drives the thumbnail check, the format label, alt and the remove label.
mimeTypestring-MIME type. Wins over the file extension when inferring mediaType.
mediaType"image" | "video" | "file"inferredimage and video render a thumbnail when src is set. file always renders a card.
fileSizenumber-Size in bytes, formatted as B, KB, MB or GB.
srcstring-Preview URL for images and videos. You own blob: URLs and must revoke them.
status"idle" | "uploading" | "error""idle"Upload state.
progressnumber-Upload percentage from 0 to 100. Omit for an indeterminate spinner.
errorReact.ReactNode-Message for status="error". Falls back to "Upload failed".
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Custom parts. Defaults to Preview and Info.

Variants

VariantOptionsDefaultDescription
size"sm" "md""md"Tile scale. md is a 64px tile, sm a 40px tile. Cascades to every part.

The media or file layout is derived from mediaType and src and exposed as data-layout. It is not a prop.

ChatAttachment.Preview

The thumbnail surface, or the icon slot on a card. Extends React.ComponentProps<"div">. Without children it renders the image, the video or Icon. On a thumbnail it also draws the upload overlay, unless asChild is set.

Props

PropTypeDefaultDescription
asChildbooleanfalseMerge the preview props onto a single child element, via Radix Slot.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Custom preview content.

ChatAttachment.Icon

The icon wrapper on a card. Extends React.ComponentProps<"span">. Defaults to a FileFormat for the inferred format, 40px at md and 28px at sm. A plain icon passed as a child is sized to 20px.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Replaces the FileFormat icon.

ChatAttachment.Info

The text column on a card, or the hover overlay on a thumbnail. Extends React.ComponentProps<"div">. Defaults to Name plus Meta on a card and Name alone on a thumbnail.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Custom text content.

ChatAttachment.Name

The truncated file name. Extends React.ComponentProps<"span">.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNodenameReplaces the name text.

ChatAttachment.Meta

The secondary line. Extends React.ComponentProps<"div">. Shows the formatted size when idle, the progress ring and percentage while uploading, and the error icon and message on error. Renders nothing when there is no content.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Replaces the status-driven content.

Variants

VariantOptionsDefaultDescription
status"idle" "uploading" "error""idle"Read from the root. Sets the text color to body, primary or error.

ChatAttachment.Remove

The remove button. Extends the CloseButton props with appearance="ghost". On a thumbnail it floats in the top end corner and appears on hover or focus.

Props

PropTypeDefaultDescription
aria-labelstring"Remove {name}"Accessible name.
size"xs" | "sm" | "md" | "lg" | "xl" | "2xl""md" or "sm"md on a md card, sm everywhere else.
appearance"solid" | "outline" | "ghost" | "soft""ghost"CloseButton fill.
onClickReact.MouseEventHandler<HTMLButtonElement>-Remove the item from your state here.
classNamestring-Tailwind classes merged via cn().

ChatAttachmentGroup

A wrapping row of tiles. Extends React.ComponentProps<"div">.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-ChatAttachment elements.

ChatAttachmentInput

Provider that renders the hidden <input type="file"> and no wrapper element. Also exported as ChatAttachmentInputRoot and ChatAttachmentInput.Root.

Props

PropTypeDefaultDescription
acceptstring-Native accept syntax. Enforced for picked, dropped and pasted files.
multiplebooleantrueWhen false, only the first valid file is kept.
maxSizenumber-Largest accepted file in bytes.
disabledbooleanfalseDisables the trigger, the drop target and paste handling.
onFilesSelected(files: File[]) => void-Accepted files from a pick, drop or paste.
onFilesRejected(rejections: ChatAttachmentRejection[]) => void-Filtered-out files with a reason.
childrenReact.ReactNode-The dropzone, trigger and anything else.

ChatAttachmentInput.Trigger

Opens the file picker. Extends React.ComponentProps<"button">. Calling event.preventDefault() in onClick keeps the picker closed.

Props

PropTypeDefaultDescription
asChildbooleanfalseMerge the trigger props onto a single child, such as Button or PromptInput.Action.
typestring"button"Native button type.
disabledboolean-Also disabled when the input is disabled.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Button content.

ChatAttachmentInput.Dropzone

Accepts dropped files and pasted images in the capture phase, so a textarea inside still receives its own events. Extends React.ComponentProps<"div">.

Props

PropTypeDefaultDescription
asChildbooleanfalseAttach the behavior to a single child, such as PromptInput.Shell. The child keeps its own classes.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Zone content.

Helpers

inferChatAttachmentMediaType(mimeType?, name?) returns "image", "video" or "file". inferChatAttachmentFormat(mimeType?, name?) returns the FileFormat label (extension, then MIME type, then "FILE"). formatChatAttachmentSize(bytes?) returns the size string or undefined. chatAttachmentVariants, chatAttachmentGroupVariants and chatAttachmentDropzoneVariants are the CVA recipes.

Types

type ChatAttachmentMediaType = "image" | "video" | "file"
type ChatAttachmentLayout = "media" | "file"
type ChatAttachmentSize = "sm" | "md"
type ChatAttachmentStatus = "idle" | "uploading" | "error"
 
type ChatAttachmentRejection = {
  file: File
  /** type fails accept, size exceeds maxSize, count is past the first file when multiple is off */
  reason: "type" | "size" | "count"
}