Skip to content
GitHub

Attachment

Displays a file or image attachment with media, metadata, upload state, and actions.

Workspace
workspace.pngPNG · 820 KB
Desk
desk-reference.jpgJPG · 1.1 MB
Office
office-reference.jpgJPG · 940 KB
sales-dashboard.pdfUploading · 64%
message-renderer.tsxTypeScript · 12 KB
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from "@/components/ui/pxl/attachment";
import { Spinner } from "@/components/ui/pxl/spinner";
const images = [
{
name: "workspace.png",
meta: "PNG · 820 KB",
src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
alt: "Workspace",
},
{
name: "desk-reference.jpg",
meta: "JPG · 1.1 MB",
src: "https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80",
alt: "Desk",
},
{
name: "office-reference.jpg",
meta: "JPG · 940 KB",
src: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80",
alt: "Office",
},
];
export default function AttachmentDemo() {
return (
<div className="mx-auto flex w-full max-w-sm flex-col gap-3 py-12">
<AttachmentGroup>
{images.map((image) => (
<Attachment key={image.name} orientation="vertical">
<AttachmentMedia variant="image">
<img src={image.src} alt={image.alt} />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>{image.name}</AttachmentTitle>
<AttachmentDescription>{image.meta}</AttachmentDescription>
</AttachmentContent>
</Attachment>
))}
</AttachmentGroup>
<Attachment state="uploading" className="w-full">
<AttachmentMedia>
<Spinner />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>sales-dashboard.pdf</AttachmentTitle>
<AttachmentDescription>Uploading · 64%</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Cancel upload">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M7 19H5v-2h2v2Zm12 0h-2v-2h2v2ZM9 15v2H7v-2h2Zm8 2h-2v-2h2v2Zm-6-2H9v-2h2v2Zm4 0h-2v-2h2v2Zm-2-2h-2v-2h2v2Zm-2-2H9V9h2v2Zm4 0h-2V9h2v2ZM9 9H7V7h2v2Zm8 0h-2V7h2v2ZM7 7H5V5h2v2Zm12 0h-2V5h2v2Z" />
</svg>
</AttachmentAction>
</AttachmentActions>
</Attachment>
<Attachment className="w-full">
<AttachmentMedia>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M18 22H6v-2h12v2ZM6 20H4V4h2v16ZM16 4h-2v4h4V6h2v14h-2V10h-6V4H6V2h10v2Zm0 14H8v-2h8v2Zm0-4H8v-2h8v2Zm-6-4H8V8h2v2Zm8-4h-2V4h2v2Z" />
</svg>
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>message-renderer.tsx</AttachmentTitle>
<AttachmentDescription>TypeScript · 12 KB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove message-renderer.tsx">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M7 19H5v-2h2v2Zm12 0h-2v-2h2v2ZM9 15v2H7v-2h2Zm8 2h-2v-2h2v2Zm-6-2H9v-2h2v2Zm4 0h-2v-2h2v2Zm-2-2h-2v-2h2v2Zm-2-2H9V9h2v2Zm4 0h-2V9h2v2ZM9 9H7V7h2v2Zm8 0h-2V7h2v2ZM7 7H5V5h2v2Zm12 0h-2V5h2v2Z" />
</svg>
</AttachmentAction>
</AttachmentActions>
</Attachment>
</div>
);
}

The Attachment component displays a file or image attachment, its media, name, and metadata, with optional actions and upload state. Use it for files and images in chat composers, message threads, and upload lists.

pnpm dlx shadcn@latest add pxl-ui/registry/attachment
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from "@/components/ui/pxl/attachment"
<Attachment>
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>sales-dashboard.pdf</AttachmentTitle>
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove sales-dashboard.pdf">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>

Use the following composition to build an attachment:

Attachment
├── AttachmentMedia
├── AttachmentContent
│ ├── AttachmentTitle
│ └── AttachmentDescription
├── AttachmentActions
│ └── AttachmentAction
└── AttachmentTrigger

Use AttachmentGroup to lay out multiple attachments in a scrollable row:

AttachmentGroup
├── Attachment
└── Attachment
  • Icon and image media through AttachmentMedia
  • Upload states: idle, uploading, processing, error, and done with built-in styling and a shimmer while in progress
  • Three sizes and horizontal or vertical orientation
  • A full-card AttachmentTrigger that opens a link or dialog while the actions stay independently clickable
  • Scrollable, snapping AttachmentGroup with an edge fade
  • Customizable styling through the className prop on every part

Set variant="image" on AttachmentMedia and render an <img> inside it. Use orientation="vertical" to stack the media above the content.

Set state to reflect the upload lifecycle. uploading and processing shimmer the title, and error switches to a destructive treatment.

Use size to switch between default, sm, and xs.

Wrap attachments in AttachmentGroup to lay them out in a horizontally scrollable, snapping row with an edge fade.

Add an AttachmentTrigger to make the whole card open a link or dialog. It fills the card behind the actions, so the actions stay clickable.

<Dialog>
<Attachment>
{/* media, content, actions */}
<DialogTrigger
render={<AttachmentTrigger aria-label="Preview research-summary.pdf" />}
/>
</Attachment>
<DialogContent>{/* ... */}</DialogContent>
</Dialog>

AttachmentAction renders a Button, and AttachmentTrigger renders a real <button> (or your element via render). Follow the guidance below so both are operable and announced.

AttachmentAction is usually icon-only, so give each one an aria-label describing the action and its target.

<AttachmentAction aria-label="Remove sales-dashboard.pdf">
<XIcon />
</AttachmentAction>

AttachmentTrigger covers the card with no text of its own, so give it an aria-label for what activating it does.

<AttachmentTrigger
render={
<a
href={url}
target="_blank"
rel="noreferrer"
aria-label="Open workspace.png"
/>
}
/>

The trigger sits behind the actions in the stacking order, so an AttachmentAction and the AttachmentTrigger never trap each other — both remain separately focusable and clickable.

An AttachmentGroup scrolls horizontally. When its attachments are interactive: a trigger or actions, keyboard users reach off-screen items by tabbing to them. For a row of presentational attachments, make the group itself focusable and scrollable by adding tabIndex={0}, role="group", and an aria-label.

The error state uses a destructive color. Keep the failure reason in AttachmentDescription so the state is not conveyed by color alone.

The root attachment container.

Prop Type Default Description
state "idle" | "uploading" | "processing" | "error" | "done" "done" The upload state. Drives styling and the shimmer.
size "default" | "sm" | "xs" "default" The attachment size.
orientation "horizontal" | "vertical" "horizontal" Lay the media beside or above the content.
className string - Additional classes to apply to the root element.

The media slot for an icon or image preview.

Prop Type Default Description
variant "icon" | "image" "icon" Whether the media holds an icon or an <img>.
className string - Additional classes to apply to the media slot.

Wraps the title and description.

Prop Type Default Description
className string - Additional classes to apply to the content slot.

The attachment name. Shimmers while the attachment is uploading or processing.

Prop Type Default Description
className string - Additional classes to apply to the title.

Secondary metadata such as the file type, size, or upload status.

Prop Type Default Description
className string - Additional classes to apply to the description.

A container for one or more actions, aligned to the end of the attachment.

Prop Type Default Description
className string - Additional classes to apply to the actions.

An action button. Renders a Button and accepts all of its props.

Prop Type Default Description
size Button["size"] "icon-xs" The button size.
...props React.ComponentProps<typeof Button> - Props spread to the underlying Button.

A full-card overlay that activates the attachment. Renders a <button> by default.

Prop Type Default Description
render ReactElement | function - Render as a different element, such as a link.
...props React.ComponentProps<"button"> - Props spread to the trigger element.

Lays out attachments in a horizontally scrollable, snapping row.

Prop Type Default Description
className string - Additional classes to apply to the group.