Attachment
Displays a file or image attachment with media, metadata, upload state, and actions.
import { FileCodeIcon, XIcon } from "lucide-solid";import { For } from "solid-js";
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentTitle,} from "~/components/ui/attachment";import { Spinner } from "~/components/ui/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 class="mx-auto flex w-full max-w-sm flex-col gap-3 py-12"> <AttachmentGroup> <For each={images}> {(image) => ( <Attachment orientation="vertical"> <AttachmentMedia variant="image"> <img src={image.src} alt={image.alt} /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>{image.name}</AttachmentTitle> <AttachmentDescription>{image.meta}</AttachmentDescription> </AttachmentContent> </Attachment> )} </For> </AttachmentGroup> <Attachment state="uploading" class="w-full"> <AttachmentMedia> <Spinner /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>sales-dashboard.pdf</AttachmentTitle> <AttachmentDescription>Uploading · 64%</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Cancel upload"> <XIcon /> </AttachmentAction> </AttachmentActions> </Attachment> <Attachment class="w-full"> <AttachmentMedia> <FileCodeIcon /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>message-renderer.tsx</AttachmentTitle> <AttachmentDescription>TypeScript · 12 KB</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Remove message-renderer.tsx"> <XIcon /> </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.
Installation
Usage
import FileTextIcon from "lucide-solid/icons/file-text";import XIcon from "lucide-solid/icons/x";import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle,} from "~/components/ui/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>Composition
Use the following composition to build an attachment:
Attachment├── AttachmentMedia├── AttachmentContent│ ├── AttachmentTitle│ └── AttachmentDescription├── AttachmentActions│ └── AttachmentAction└── AttachmentTriggerUse AttachmentGroup to lay out multiple attachments in a scrollable row:
AttachmentGroup├── Attachment└── AttachmentFeatures
- Icon and image media through
AttachmentMedia. - Upload states:
idle,uploading,processing,error, anddonewith built-in styling and a shimmer while in progress. - Three sizes and horizontal or vertical orientation.
- A full-card
AttachmentTriggerthat opens a link or dialog while the actions stay independently clickable. - Scrollable, snapping
AttachmentGroupwith an edge fade. - Customizable styling through the
classprop on every part.
Image
Set variant="image" on AttachmentMedia and render an <img> inside it. Use
orientation="vertical" to stack the media above the content.
import { XIcon } from "lucide-solid";import { For } from "solid-js";
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentTitle, AttachmentTrigger,} from "~/components/ui/attachment";
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 AttachmentImage() { return ( <div class="mx-auto w-full max-w-sm py-12"> <AttachmentGroup class="w-full"> <For each={images}> {(image) => ( <Attachment orientation="vertical"> <AttachmentMedia variant="image"> <img src={image.src} alt={image.alt} /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>{image.name}</AttachmentTitle> <AttachmentDescription>{image.meta}</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label={`Remove ${image.name}`}> <XIcon /> </AttachmentAction> </AttachmentActions> <AttachmentTrigger as="a" href={image.src} target="_blank" rel="noreferrer" aria-label={`Open ${image.name}`} /> </Attachment> )} </For> </AttachmentGroup> </div> );}States
Set state to reflect the upload lifecycle. uploading and processing shimmer the title, and
error switches to a destructive treatment.
import { CheckIcon, ClockIcon, FileTextIcon, FileWarningIcon, RefreshCwIcon, XIcon,} from "lucide-solid";
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle,} from "~/components/ui/attachment";import { Spinner } from "~/components/ui/spinner";
export default function AttachmentStates() { return ( <div class="mx-auto flex w-full max-w-sm flex-col gap-2 py-12"> <Attachment state="idle" class="w-full"> <AttachmentMedia> <ClockIcon /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>selected-file.pdf</AttachmentTitle> <AttachmentDescription>Ready to upload</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Remove selected-file.pdf"> <XIcon /> </AttachmentAction> </AttachmentActions> </Attachment> <Attachment state="uploading" class="w-full"> <AttachmentMedia> <Spinner /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>design-system.zip</AttachmentTitle> <AttachmentDescription>Uploading · 64%</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Cancel upload"> <XIcon /> </AttachmentAction> </AttachmentActions> </Attachment> <Attachment state="processing" class="w-full"> <AttachmentMedia> <FileTextIcon /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>market-research.pdf</AttachmentTitle> <AttachmentDescription>Processing document</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Remove market-research.pdf"> <XIcon /> </AttachmentAction> </AttachmentActions> </Attachment> <Attachment state="error" class="w-full"> <AttachmentMedia> <FileWarningIcon /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>financial-model.xlsx</AttachmentTitle> <AttachmentDescription>Upload failed. Try again.</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Retry upload"> <RefreshCwIcon /> </AttachmentAction> <AttachmentAction aria-label="Remove financial-model.xlsx"> <XIcon /> </AttachmentAction> </AttachmentActions> </Attachment> <Attachment state="done" class="w-full"> <AttachmentMedia> <CheckIcon /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>uploaded-report.pdf</AttachmentTitle> <AttachmentDescription>Uploaded · 1.8 MB</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Remove uploaded-report.pdf"> <XIcon /> </AttachmentAction> </AttachmentActions> </Attachment> </div> );}Sizes
Use size to switch between default, sm, and xs.
import { FileTextIcon } from "lucide-solid";
import { Attachment, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle,} from "~/components/ui/attachment";
export default function AttachmentSizes() { return ( <div class="mx-auto flex w-full max-w-sm flex-col gap-3 py-12"> <Attachment size="default" class="w-full"> <AttachmentMedia> <FileTextIcon /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>Default attachment</AttachmentTitle> <AttachmentDescription>PDF · 2.4 MB</AttachmentDescription> </AttachmentContent> </Attachment> <Attachment size="sm" class="w-full"> <AttachmentMedia> <FileTextIcon /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>Small attachment</AttachmentTitle> <AttachmentDescription>PDF · 2.4 MB</AttachmentDescription> </AttachmentContent> </Attachment> <Attachment size="xs" class="w-full"> <AttachmentMedia> <FileTextIcon /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>Extra small attachment</AttachmentTitle> </AttachmentContent> </Attachment> </div> );}Group
Wrap attachments in AttachmentGroup to lay them out in a horizontally scrollable, snapping row
with an edge fade.
import { FileCodeIcon, FileTextIcon, TableIcon, XIcon } from "lucide-solid";import type { Component } from "solid-js";import { For, Show } from "solid-js";import { Dynamic } from "solid-js/web";
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentTitle,} from "~/components/ui/attachment";
type Item = { name: string; meta: string; icon?: Component; src?: string;};
const items: Item[] = [ { name: "briefing-notes.pdf", meta: "PDF · 1.4 MB", icon: FileTextIcon }, { name: "workspace.png", meta: "PNG · 820 KB", src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80", }, { name: "customers.csv", meta: "CSV · 18 KB", icon: TableIcon }, { name: "renderer.tsx", meta: "TSX · 12 KB", icon: FileCodeIcon },];
export default function AttachmentGroupDemo() { return ( <div class="mx-auto w-full max-w-sm py-12"> <AttachmentGroup class="w-full"> <For each={items}> {(item) => ( <Attachment class="w-64"> <Show when={item.src} fallback={ <Show when={item.icon}> {(Icon) => ( <AttachmentMedia> <Dynamic component={Icon()} /> </AttachmentMedia> )} </Show> } > {(src) => ( <AttachmentMedia variant="image"> <img src={src()} alt={item.name} /> </AttachmentMedia> )} </Show> <AttachmentContent> <AttachmentTitle>{item.name}</AttachmentTitle> <AttachmentDescription>{item.meta}</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label={`Remove ${item.name}`}> <XIcon /> </AttachmentAction> </AttachmentActions> </Attachment> )} </For> </AttachmentGroup> </div> );}Trigger
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.
import { CopyIcon, FileSearchIcon, XIcon } from "lucide-solid";
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle, AttachmentTrigger,} from "~/components/ui/attachment";import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger,} from "~/components/ui/dialog";
export default function AttachmentTriggerDemo() { return ( <div class="mx-auto w-full max-w-sm py-12"> <Dialog> <Attachment class="w-full"> <AttachmentMedia> <FileSearchIcon /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>research-summary.pdf</AttachmentTitle> <AttachmentDescription>Open preview dialog</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Copy link"> <CopyIcon /> </AttachmentAction> <AttachmentAction aria-label="Remove research-summary.pdf"> <XIcon /> </AttachmentAction> </AttachmentActions> <AttachmentTrigger as={DialogTrigger} aria-label="Preview research-summary.pdf" /> </Attachment> <DialogContent class="sm:max-w-md"> <DialogHeader> <DialogTitle>research-summary.pdf</DialogTitle> <DialogDescription> The attachment trigger fills the card and opens the dialog, while the actions stay independently clickable above it. </DialogDescription> </DialogHeader> </DialogContent> </Dialog> </div> );}<Dialog> <Attachment> {/* media, content, actions */} <AttachmentTrigger as={DialogTrigger} aria-label="Preview research-summary.pdf" /> </Attachment> <DialogContent>{/* ... */}</DialogContent></Dialog>Accessibility
AttachmentAction renders a Button, and AttachmentTrigger renders a real <button> (or your
element via as). Follow the guidance below so both are operable and announced.
Label icon-only actions
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>Label the trigger
AttachmentTrigger covers the card with no text of its own, so give it an aria-label for what
activating it does.
<AttachmentTrigger as="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.
Keyboard scrolling
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.
Meaning beyond color
The error state uses a destructive color. Keep the failure reason in AttachmentDescription so
the state is not conveyed by color alone.
API Reference
Attachment
The root attachment container.
AttachmentMedia
The media slot for an icon or image preview.
AttachmentContent
Wraps the title and description.
AttachmentTitle
The attachment name. Shimmers while the attachment is uploading or processing.
AttachmentDescription
Secondary metadata such as the file type, size, or upload status.
AttachmentActions
A container for one or more actions, aligned to the end of the attachment.
AttachmentAction
An action button. Renders a Button and accepts all of its props.
AttachmentTrigger
A full-card overlay that activates the attachment. Renders a <button> by default.
AttachmentGroup
Lays out attachments in a horizontally scrollable, snapping row.