Attachment
An Attachment shows users a file on a message, with its name, its upload state, and its actions.
When to Use
- for a file sent with a Message, such as a signed contract under the text
- for a file that is still uploading
- for a file that opens when users click it, with
AttachmentTrigger, such as an estimate that opens a preview in a Dialog - as an
AttachmentGroup, for several files in one row
When Not to Use
- to pick a file from the device (use an Input with
type="file") - to show how far an upload has come as a bar (use the Progress component)
- for a record that isn’t a file, like a client or a project (use the Item component)
Anatomy
| Element | Usage |
|---|---|
| Title* | Optional |
| Media* | Optional |
| Action | Optional |
| Description | Optional |
| Container | Required |
| Trigger | Optional |
| Focus outline | Keyboard focus on the trigger |
* An Attachment has a media tile, a title, or both.
AttachmentTrigger is a button with no content that covers the whole card. AttachmentActions is positioned above it, so users can still click each action.
Variants
Set the variant on AttachmentMedia.
| Variant | Purpose |
|---|---|
icon | An icon for the kind of file |
image | A preview of an image file |
At image, the tile crops the img inside it to a square.
Sizes
| Size | Height |
|---|---|
default | 58px |
sm | 47px |
xs | 43px |
The heights are those of a horizontal Attachment with a title and a description.
States
state marks each stage of an upload, from idle to done. Attachment doesn’t track the upload: the product sets state, writes the description, and chooses the icon.
Uploading and Processing
Show the progress in the description, such as “Uploading, 64%”.
Error
Color can’t be the only sign of the failure, so say what failed in the description.
Behavior
A horizontal Attachment is as wide as its content, from 160px to the width of its container. The title and the description stay on one line each, and they are truncated with an ellipsis when they don’t fit.
Orientation
To stack the media above the text, set orientation="vertical". The card is 120px wide, or 96px when it has no text.
Group
AttachmentGroup places Attachments in a row. When the row is wider than its container, it scrolls sideways, snaps each Attachment to the start edge, and hides its scrollbar.
Content
| String | Rule | Example | Counterexample |
|---|---|---|---|
| Title | The file’s name with its extension | “CTR-2026-007.pdf” | “Contract” |
| Description | The type and the size, or the state in words | “PDF, 2.4 MB” | “Done” |
Description, in error | What failed and what to do next | “The upload failed. Try again.” | “Error” |
aria-label of an action | The action and the file’s name | “Remove CTR-2026-007.pdf” | “Remove” |
aria-label of the trigger | What the click does and the file’s name | “Preview EST-2026-021.pdf” | “Open” |
Accessibility
An Attachment is a div with no role. Its actions and its trigger are native buttons, so the browser provides their role and their keys.
An AttachmentAction contains only an icon, and an AttachmentTrigger has no content, so both need an aria-label
for screen readers to announce them, as WCAG 2.2 SC 4.1.2 Name, Role, Value requires. Show the name of the action in a
Tooltip as well.
An Attachment in the error state must say in its description what failed so that users who can’t see the red tint
know about the failure. WCAG 2.2 SC 1.4.1 Use of Color doesn’t allow color to be the only sign of it.
If the Attachments in an AttachmentGroup have no buttons, the group needs tabIndex={0}, role="group", and an
aria-label so that keyboard users can scroll the row, as WCAG 2.2 SC 2.1.1 Keyboard requires. If tabIndex isn’t set,
nothing in the row is focusable in WebKit.
If the title next to the media names the file, set alt="" on the img. Set aria-hidden on an icon in the media, as for an icon in the Item component.
Installation
npx shadcn@latest add @summit/attachmentThe CLI also adds @summit/button.
Usage
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from '@/components/ui/attachment';
import { DownloadIcon, FileTextIcon } from '@phosphor-icons/react';
<Attachment>
<AttachmentMedia>
<FileTextIcon aria-hidden />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>CTR-2026-007.pdf</AttachmentTitle>
<AttachmentDescription>PDF, 2.4 MB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Download CTR-2026-007.pdf">
<DownloadIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>;With an Image
Set variant="image" on the media and orientation="vertical" on the Attachment. For an action over the image, set variant="secondary" so that the action has a fill.
import { XIcon } from '@phosphor-icons/react';
<Attachment orientation="vertical">
<AttachmentMedia variant="image">
<img src="/site-survey.png" alt="" />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>site-survey.png</AttachmentTitle>
<AttachmentDescription>PNG, 820 KB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction variant="secondary" aria-label="Remove site-survey.png">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>;While It Uploads
Set state, show a Spinner in the media, and show the progress in the description.
import { Spinner } from '@/components/ui/spinner';
<Attachment state="uploading">
<AttachmentMedia>
<Spinner />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>CTR-2026-007.pdf</AttachmentTitle>
<AttachmentDescription>Uploading, 64%</AttachmentDescription>
</AttachmentContent>
</Attachment>;With a Trigger
Pass AttachmentTrigger to a DialogTrigger through render, and name it with aria-label.
import { AttachmentTrigger } from '@/components/ui/attachment';
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
<Dialog>
<Attachment>
<AttachmentContent>
<AttachmentTitle>EST-2026-021.pdf</AttachmentTitle>
<AttachmentDescription>Opens a preview</AttachmentDescription>
</AttachmentContent>
<DialogTrigger render={<AttachmentTrigger aria-label="Preview EST-2026-021.pdf" />} />
</Attachment>
<DialogContent>
<DialogTitle>EST-2026-021.pdf</DialogTitle>
</DialogContent>
</Dialog>;A Group
In this example, every Attachment has a remove button, so users can reach all of them with Tab, and the group doesn’t need tabIndex.
import { AttachmentGroup } from '@/components/ui/attachment';
import { XIcon } from '@phosphor-icons/react';
<AttachmentGroup>
<Attachment>
<AttachmentContent>
<AttachmentTitle>CTR-2026-007.pdf</AttachmentTitle>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove CTR-2026-007.pdf">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
<Attachment>
<AttachmentContent>
<AttachmentTitle>INV-2026-014.pdf</AttachmentTitle>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove INV-2026-014.pdf">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
</AttachmentGroup>;API Reference
Other props are passed to the element of each part.
Attachment
| Prop | Type | Default | Description |
|---|---|---|---|
state | 'idle' | 'uploading' | 'processing' | 'error' | 'done' | 'done' | The stage of the upload |
size | 'default' | 'sm' | 'xs' | 'default' | The padding, the media size, and the text size |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Whether the media is next to the text or above it |
AttachmentMedia
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'icon' | 'image' | 'icon' | Whether the tile contains an icon or an img |
AttachmentAction
AttachmentAction accepts the props of a Button. By default, its variant is 'ghost', and its size is 'icon-xs'.
AttachmentTrigger
AttachmentTrigger sets type="button" unless render is passed. See the Base UI useRender documentation for render.