Attachment
Show a file or an image attached to a message or a draft, with its upload state.
Installation
pnpm dlx shadcn@4.18.0 add @siteplane/attachmentComplete the one-time setup first. Registry JSON lists source files and dependencies.
For this page’s examples, also install:
pnpm dlx shadcn@4.18.0 add @siteplane/bubble @siteplane/dialog @siteplane/file-type-icon @siteplane/message @siteplane/prompt-input @siteplane/spinnerUsage
import { XIcon } from "lucide-react";
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from "@/siteplane/components/attachment";
import { FileTypeIcon } from "@/siteplane/components/file-type-icon";
<Attachment>
<AttachmentMedia>
<FileTypeIcon name="q3-review.pdf" size={32} />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>q3-review.pdf</AttachmentTitle>
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove q3-review.pdf">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>;The card shows the state it is given. The app owns the files and their uploads.
Composition
AttachmentGroup optional, a row of cards that scrolls
└── Attachment
├── AttachmentMedia an icon, a File Type Icon or an image
├── AttachmentContent
│ ├── AttachmentTitle
│ └── AttachmentDescription
├── AttachmentActions
│ └── AttachmentAction remove, retry, cancel or download
└── AttachmentTrigger optional, over the whole cardStates
state follows a file through its upload. idle waits to upload and draws a dashed edge. While the file is uploading or processing, its title shimmers and the card is aria-busy. error draws the destructive edge and turns the description and the media tile to the destructive color. done, the default, is a file that is there.
Say the state in the description as well, such as “Uploading · 64%” or “Upload failed”, because color and motion alone do not reach everyone. Give a failed card a Retry action and an uploading card an action that cancels it.
Media
AttachmentMedia holds the picture of the file. With variant="icon", the default, it is a square tile for a lucide icon or a Spinner. A File Type Icon sits in it without the tile: size={32} fits a medium card and size={24} a small one. With variant="image", an img fills it as a thumbnail, cropped to the square. A thumbnail shows dimmed while its file uploads or is processed.
Sizes
size="md", the default, has a 40 px media tile and 14 px text. size="sm" has a 32 px tile and 12 px text, for cards inside a field such as the Prompt Input. A name that does not fit ends in an ellipsis and keeps its full text in a title attribute, so the actions stay in the card.
Orientation
orientation="horizontal", the default, puts the media beside the text. orientation="vertical" stacks the media over the text, for images. The card is then 120 px wide with text and 96 px with the media alone, and its actions float over the top-end corner of the image as the opaque default Button, which reads on any picture. The first example on this page shows vertical cards with text, and the message example one with the image alone.
Actions
AttachmentAction is an icon Button in AttachmentActions for an action such as remove, retry, cancel or download. Beside the text it is the bare muted action in a 24 px target. It has no text, so its aria-label names the action and the file, such as “Remove contracts.pdf”. Button props pass through: a variant replaces the default look, and render makes the action a download link.
Trigger
AttachmentTrigger lies over the whole card, behind the actions, and opens the file or a preview of it. Put it in the render of a DialogTrigger for a preview, as here, or give it render for a link. It has no text, so its aria-label says what it does, such as “Preview pipeline-overview.png”. The actions above it stay clickable.
Group
AttachmentGroup is a row of cards that scrolls sideways, snaps card by card and fades the edge that has more to show. Cards that join or leave fade, and the others glide to their new place. Give every Attachment a stable key, such as an id and never the index, because the key tells the row which card has left. Name the row with label, such as “Attachments”, so it is a labeled group. In a group a card is at most 16 rem wide, so a long name ends in an ellipsis there too; a max-w class on the card widens it.
A card is inert while it leaves, so its button cannot keep focus. Move focus before you remove a card, for example to the message field.
In a message
A sent turn shows its files above its text. Put an AttachmentGroup in the content of a user turn, above its Bubble. In a Message with align="end" the group is only as wide as its cards, so they sit at the end of the turn like the bubble, and a long row still scrolls. A sent file needs no remove action; a trigger can open it.
In the composer
In a Prompt Input, the group goes in PromptInputAttachments, the row above the field, with small cards. That page also shows files that upload and go with the message.
AI SDK
A file part of a UIMessage from useChat is an Attachment. The examples on this page run on local state; the code below renders the parts of a message instead.
File parts
A part whose media type is image/* shows its url as a thumbnail, and any other part shows a File Type Icon chosen by its filename. A sent message shows its files in a group above its text.
import type { FileUIPart, UIMessage } from "ai";
import {
Attachment,
AttachmentContent,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from "@/siteplane/components/attachment";
import { FileTypeIcon } from "@/siteplane/components/file-type-icon";
function FileAttachment({ part }: { part: FileUIPart }) {
const name = part.filename ?? "Attachment";
if (part.mediaType.startsWith("image/"))
return (
<Attachment orientation="vertical">
<AttachmentMedia variant="image">
<img alt={name} src={part.url} />
</AttachmentMedia>
</Attachment>
);
return (
<Attachment>
<AttachmentMedia>
<FileTypeIcon name={name} size={32} />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>{name}</AttachmentTitle>
</AttachmentContent>
</Attachment>
);
}
// In the MessageContent of a user turn, above its Bubble:
export function MessageFiles({ message }: { message: UIMessage }) {
const files = message.parts.filter(
(part): part is FileUIPart => part.type === "file",
);
if (files.length === 0) return null;
return (
<AttachmentGroup label="Attachments">
{files.map((part, index) => (
<FileAttachment key={`${message.id}-${index}`} part={part} />
))}
</AttachmentGroup>
);
}Upload states
A file part has no upload state, so a sent file is done, the default. The states belong to the draft while your app uploads its files: a card is uploading until the file has its URL, and error when the upload fails. The Prompt Input page turns picked files into FileUIParts and sends them with sendMessage.
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
Attachment. | idle | uploading | processing | error | done | done | Where the file is in its upload. uploading and processing shimmer the title and set aria-busy, error draws the destructive edge, and idle draws a dashed one. |
Attachment. | sm | md | md | md has a 40 px media tile and 14 px text; sm has a 32 px tile and 12 px text, for cards inside a field. |
Attachment. | horizontal | vertical | horizontal | vertical stacks the media over the text, for images. The card is 120 px wide with text and 96 px with the media alone. |
Attachment | icon | image | icon | icon is a square tile for an icon, and shows a File Type Icon without the tile; image holds an img that fills it. |
Attachment | string | required | Names the action and its file, such as Remove brief.pdf. |
Attachment | Button props | muted inline icon Button | Beside the text the bare muted action in a 24 px target, over the image of a vertical card the opaque default Button. Button props replace the look. |
Attachment | string | required | Says what activating the card does, such as Open brief.pdf. |
Attachment | ReactElement | render function | button | Renders the trigger as another element, such as a link. In the render of a DialogTrigger it opens the dialog. |
Attachment | string | unset | Names the row for assistive technology and makes it a group. A row that scrolls should have one. |
Attachment, AttachmentMedia, AttachmentContent, AttachmentActions and
AttachmentGroup accept the native props of a div, and AttachmentTitle and
AttachmentDescription those of a span. A title or description that is a string
is also the element’s title attribute, so a name cut off by its ellipsis can
still be read in full. AttachmentAction and AttachmentTrigger accept the Button
props, with aria-label required. AttachmentGroup lays its cards out in a
Scroll Area with an edge fade and no scrollbar, as a Motion list. The types
AttachmentProps, AttachmentState, AttachmentSize, AttachmentOrientation,
AttachmentMediaProps, AttachmentActionProps, AttachmentTriggerProps and
AttachmentGroupProps are exported.
Usage notes
The card uploads nothing and reads no file. The app starts, retries and cancels the uploads and gives each card its state and its details in words. A File Type Icon takes the type from the file name only, so pass the thumbnail of an image yourself.
While the row of a group overflows, its scroll area joins the tab order and scrolls with the arrow keys. Under reduced motion the shimmer of a title stands still, and cards join and leave a group at once.