ZaidanHomeDocsComponentsBlocksChartsTypesetCreate

Command Palette

Search for a command to run...

GitHub
New
Sections
  • Introduction
  • Components
  • Blocks
  • Installation
  • Customization
  • Dark Mode
  • Typeset
  • Zaidan Skills
  • FAQ
  • Roadmap
  • Changelog
Installation
  • Astro
  • Manual
  • Solid Start
  • TanStack Router
  • TanStack Start
  • Vite
Blocks
  • Data Grid
  • Event Calendar
  • Filters
  • Gantt
  • Image Crop
  • Kanban
  • Message Scroller
  • Questionnaire
  • Sortable
Components
  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Marker
  • Menubar
  • Message
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip

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
1
import { FileCodeIcon, XIcon } from "lucide-solid";
2
import { For } from "solid-js";
3
4
import {
5
Attachment,
6
AttachmentAction,
7
AttachmentActions,
8
AttachmentContent,
9
AttachmentDescription,
10
AttachmentGroup,
11
AttachmentMedia,
12
AttachmentTitle,
13
} from "~/components/ui/attachment";
14
import { Spinner } from "~/components/ui/spinner";
15
16
const images = [
17
{
18
name: "workspace.png",
19
meta: "PNG · 820 KB",
20
src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
21
alt: "Workspace",
22
},
23
{
24
name: "desk-reference.jpg",
25
meta: "JPG · 1.1 MB",
26
src: "https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80",
27
alt: "Desk",
28
},
29
{
30
name: "office-reference.jpg",
31
meta: "JPG · 940 KB",
32
src: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80",
33
alt: "Office",
34
},
35
];
36
37
export default function AttachmentDemo() {
38
return (
39
<div class="mx-auto flex w-full max-w-sm flex-col gap-3 py-12">
40
<AttachmentGroup>
41
<For each={images}>
42
{(image) => (
43
<Attachment orientation="vertical">
44
<AttachmentMedia variant="image">
45
<img src={image.src} alt={image.alt} />
46
</AttachmentMedia>
47
<AttachmentContent>
48
<AttachmentTitle>{image.name}</AttachmentTitle>
49
<AttachmentDescription>{image.meta}</AttachmentDescription>
50
</AttachmentContent>
51
</Attachment>
52
)}
53
</For>
54
</AttachmentGroup>
55
<Attachment state="uploading" class="w-full">
56
<AttachmentMedia>
57
<Spinner />
58
</AttachmentMedia>
59
<AttachmentContent>
60
<AttachmentTitle>sales-dashboard.pdf</AttachmentTitle>
61
<AttachmentDescription>Uploading · 64%</AttachmentDescription>
62
</AttachmentContent>
63
<AttachmentActions>
64
<AttachmentAction aria-label="Cancel upload">
65
<XIcon />
66
</AttachmentAction>
67
</AttachmentActions>
68
</Attachment>
69
<Attachment class="w-full">
70
<AttachmentMedia>
71
<FileCodeIcon />
72
</AttachmentMedia>
73
<AttachmentContent>
74
<AttachmentTitle>message-renderer.tsx</AttachmentTitle>
75
<AttachmentDescription>TypeScript · 12 KB</AttachmentDescription>
76
</AttachmentContent>
77
<AttachmentActions>
78
<AttachmentAction aria-label="Remove message-renderer.tsx">
79
<XIcon />
80
</AttachmentAction>
81
</AttachmentActions>
82
</Attachment>
83
</div>
84
);
85
}

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

pnpm dlx shadcn@latest add @zaidan/attachment
npx shadcn@latest add @zaidan/attachment
yarn dlx shadcn@latest add @zaidan/attachment
bunx --bun shadcn@latest add @zaidan/attachment

Usage

1
import FileTextIcon from "lucide-solid/icons/file-text";
2
import XIcon from "lucide-solid/icons/x";
3
import {
4
Attachment,
5
AttachmentAction,
6
AttachmentActions,
7
AttachmentContent,
8
AttachmentDescription,
9
AttachmentMedia,
10
AttachmentTitle,
11
} from "~/components/ui/attachment";
1
<Attachment>
2
<AttachmentMedia>
3
<FileTextIcon />
4
</AttachmentMedia>
5
<AttachmentContent>
6
<AttachmentTitle>sales-dashboard.pdf</AttachmentTitle>
7
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
8
</AttachmentContent>
9
<AttachmentActions>
10
<AttachmentAction aria-label="Remove sales-dashboard.pdf">
11
<XIcon />
12
</AttachmentAction>
13
</AttachmentActions>
14
</Attachment>

Composition

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

Features

  • 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 class prop 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.

Workspace
workspace.pngPNG · 820 KB
Desk
desk-reference.jpgJPG · 1.1 MB
Office
office-reference.jpgJPG · 940 KB
1
import { XIcon } from "lucide-solid";
2
import { For } from "solid-js";
3
4
import {
5
Attachment,
6
AttachmentAction,
7
AttachmentActions,
8
AttachmentContent,
9
AttachmentDescription,
10
AttachmentGroup,
11
AttachmentMedia,
12
AttachmentTitle,
13
AttachmentTrigger,
14
} from "~/components/ui/attachment";
15
16
const images = [
17
{
18
name: "workspace.png",
19
meta: "PNG · 820 KB",
20
src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
21
alt: "Workspace",
22
},
23
{
24
name: "desk-reference.jpg",
25
meta: "JPG · 1.1 MB",
26
src: "https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80",
27
alt: "Desk",
28
},
29
{
30
name: "office-reference.jpg",
31
meta: "JPG · 940 KB",
32
src: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80",
33
alt: "Office",
34
},
35
];
36
37
export default function AttachmentImage() {
38
return (
39
<div class="mx-auto w-full max-w-sm py-12">
40
<AttachmentGroup class="w-full">
41
<For each={images}>
42
{(image) => (
43
<Attachment orientation="vertical">
44
<AttachmentMedia variant="image">
45
<img src={image.src} alt={image.alt} />
46
</AttachmentMedia>
47
<AttachmentContent>
48
<AttachmentTitle>{image.name}</AttachmentTitle>
49
<AttachmentDescription>{image.meta}</AttachmentDescription>
50
</AttachmentContent>
51
<AttachmentActions>
52
<AttachmentAction aria-label={`Remove ${image.name}`}>
53
<XIcon />
54
</AttachmentAction>
55
</AttachmentActions>
56
<AttachmentTrigger
57
as="a"
58
href={image.src}
59
target="_blank"
60
rel="noreferrer"
61
aria-label={`Open ${image.name}`}
62
/>
63
</Attachment>
64
)}
65
</For>
66
</AttachmentGroup>
67
</div>
68
);
69
}

States

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

selected-file.pdfReady to upload
design-system.zipUploading · 64%
market-research.pdfProcessing document
financial-model.xlsxUpload failed. Try again.
uploaded-report.pdfUploaded · 1.8 MB
1
import {
2
CheckIcon,
3
ClockIcon,
4
FileTextIcon,
5
FileWarningIcon,
6
RefreshCwIcon,
7
XIcon,
8
} from "lucide-solid";
9
10
import {
11
Attachment,
12
AttachmentAction,
13
AttachmentActions,
14
AttachmentContent,
15
AttachmentDescription,
16
AttachmentMedia,
17
AttachmentTitle,
18
} from "~/components/ui/attachment";
19
import { Spinner } from "~/components/ui/spinner";
20
21
export default function AttachmentStates() {
22
return (
23
<div class="mx-auto flex w-full max-w-sm flex-col gap-2 py-12">
24
<Attachment state="idle" class="w-full">
25
<AttachmentMedia>
26
<ClockIcon />
27
</AttachmentMedia>
28
<AttachmentContent>
29
<AttachmentTitle>selected-file.pdf</AttachmentTitle>
30
<AttachmentDescription>Ready to upload</AttachmentDescription>
31
</AttachmentContent>
32
<AttachmentActions>
33
<AttachmentAction aria-label="Remove selected-file.pdf">
34
<XIcon />
35
</AttachmentAction>
36
</AttachmentActions>
37
</Attachment>
38
<Attachment state="uploading" class="w-full">
39
<AttachmentMedia>
40
<Spinner />
41
</AttachmentMedia>
42
<AttachmentContent>
43
<AttachmentTitle>design-system.zip</AttachmentTitle>
44
<AttachmentDescription>Uploading · 64%</AttachmentDescription>
45
</AttachmentContent>
46
<AttachmentActions>
47
<AttachmentAction aria-label="Cancel upload">
48
<XIcon />
49
</AttachmentAction>
50
</AttachmentActions>
51
</Attachment>
52
<Attachment state="processing" class="w-full">
53
<AttachmentMedia>
54
<FileTextIcon />
55
</AttachmentMedia>
56
<AttachmentContent>
57
<AttachmentTitle>market-research.pdf</AttachmentTitle>
58
<AttachmentDescription>Processing document</AttachmentDescription>
59
</AttachmentContent>
60
<AttachmentActions>
61
<AttachmentAction aria-label="Remove market-research.pdf">
62
<XIcon />
63
</AttachmentAction>
64
</AttachmentActions>
65
</Attachment>
66
<Attachment state="error" class="w-full">
67
<AttachmentMedia>
68
<FileWarningIcon />
69
</AttachmentMedia>
70
<AttachmentContent>
71
<AttachmentTitle>financial-model.xlsx</AttachmentTitle>
72
<AttachmentDescription>Upload failed. Try again.</AttachmentDescription>
73
</AttachmentContent>
74
<AttachmentActions>
75
<AttachmentAction aria-label="Retry upload">
76
<RefreshCwIcon />
77
</AttachmentAction>
78
<AttachmentAction aria-label="Remove financial-model.xlsx">
79
<XIcon />
80
</AttachmentAction>
81
</AttachmentActions>
82
</Attachment>
83
<Attachment state="done" class="w-full">
84
<AttachmentMedia>
85
<CheckIcon />
86
</AttachmentMedia>
87
<AttachmentContent>
88
<AttachmentTitle>uploaded-report.pdf</AttachmentTitle>
89
<AttachmentDescription>Uploaded · 1.8 MB</AttachmentDescription>
90
</AttachmentContent>
91
<AttachmentActions>
92
<AttachmentAction aria-label="Remove uploaded-report.pdf">
93
<XIcon />
94
</AttachmentAction>
95
</AttachmentActions>
96
</Attachment>
97
</div>
98
);
99
}

Sizes

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

Default attachmentPDF · 2.4 MB
Small attachmentPDF · 2.4 MB
Extra small attachment
1
import { FileTextIcon } from "lucide-solid";
2
3
import {
4
Attachment,
5
AttachmentContent,
6
AttachmentDescription,
7
AttachmentMedia,
8
AttachmentTitle,
9
} from "~/components/ui/attachment";
10
11
export default function AttachmentSizes() {
12
return (
13
<div class="mx-auto flex w-full max-w-sm flex-col gap-3 py-12">
14
<Attachment size="default" class="w-full">
15
<AttachmentMedia>
16
<FileTextIcon />
17
</AttachmentMedia>
18
<AttachmentContent>
19
<AttachmentTitle>Default attachment</AttachmentTitle>
20
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
21
</AttachmentContent>
22
</Attachment>
23
<Attachment size="sm" class="w-full">
24
<AttachmentMedia>
25
<FileTextIcon />
26
</AttachmentMedia>
27
<AttachmentContent>
28
<AttachmentTitle>Small attachment</AttachmentTitle>
29
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
30
</AttachmentContent>
31
</Attachment>
32
<Attachment size="xs" class="w-full">
33
<AttachmentMedia>
34
<FileTextIcon />
35
</AttachmentMedia>
36
<AttachmentContent>
37
<AttachmentTitle>Extra small attachment</AttachmentTitle>
38
</AttachmentContent>
39
</Attachment>
40
</div>
41
);
42
}

Group

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

briefing-notes.pdfPDF · 1.4 MB
workspace.png
workspace.pngPNG · 820 KB
customers.csvCSV · 18 KB
renderer.tsxTSX · 12 KB
1
import { FileCodeIcon, FileTextIcon, TableIcon, XIcon } from "lucide-solid";
2
import type { Component } from "solid-js";
3
import { For, Show } from "solid-js";
4
import { Dynamic } from "solid-js/web";
5
6
import {
7
Attachment,
8
AttachmentAction,
9
AttachmentActions,
10
AttachmentContent,
11
AttachmentDescription,
12
AttachmentGroup,
13
AttachmentMedia,
14
AttachmentTitle,
15
} from "~/components/ui/attachment";
16
17
type Item = {
18
name: string;
19
meta: string;
20
icon?: Component;
21
src?: string;
22
};
23
24
const items: Item[] = [
25
{ name: "briefing-notes.pdf", meta: "PDF · 1.4 MB", icon: FileTextIcon },
26
{
27
name: "workspace.png",
28
meta: "PNG · 820 KB",
29
src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
30
},
31
{ name: "customers.csv", meta: "CSV · 18 KB", icon: TableIcon },
32
{ name: "renderer.tsx", meta: "TSX · 12 KB", icon: FileCodeIcon },
33
];
34
35
export default function AttachmentGroupDemo() {
36
return (
37
<div class="mx-auto w-full max-w-sm py-12">
38
<AttachmentGroup class="w-full">
39
<For each={items}>
40
{(item) => (
41
<Attachment class="w-64">
42
<Show
43
when={item.src}
44
fallback={
45
<Show when={item.icon}>
46
{(Icon) => (
47
<AttachmentMedia>
48
<Dynamic component={Icon()} />
49
</AttachmentMedia>
50
)}
51
</Show>
52
}
53
>
54
{(src) => (
55
<AttachmentMedia variant="image">
56
<img src={src()} alt={item.name} />
57
</AttachmentMedia>
58
)}
59
</Show>
60
<AttachmentContent>
61
<AttachmentTitle>{item.name}</AttachmentTitle>
62
<AttachmentDescription>{item.meta}</AttachmentDescription>
63
</AttachmentContent>
64
<AttachmentActions>
65
<AttachmentAction aria-label={`Remove ${item.name}`}>
66
<XIcon />
67
</AttachmentAction>
68
</AttachmentActions>
69
</Attachment>
70
)}
71
</For>
72
</AttachmentGroup>
73
</div>
74
);
75
}

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.

research-summary.pdfOpen preview dialog
1
import { CopyIcon, FileSearchIcon, XIcon } from "lucide-solid";
2
3
import {
4
Attachment,
5
AttachmentAction,
6
AttachmentActions,
7
AttachmentContent,
8
AttachmentDescription,
9
AttachmentMedia,
10
AttachmentTitle,
11
AttachmentTrigger,
12
} from "~/components/ui/attachment";
13
import {
14
Dialog,
15
DialogContent,
16
DialogDescription,
17
DialogHeader,
18
DialogTitle,
19
DialogTrigger,
20
} from "~/components/ui/dialog";
21
22
export default function AttachmentTriggerDemo() {
23
return (
24
<div class="mx-auto w-full max-w-sm py-12">
25
<Dialog>
26
<Attachment class="w-full">
27
<AttachmentMedia>
28
<FileSearchIcon />
29
</AttachmentMedia>
30
<AttachmentContent>
31
<AttachmentTitle>research-summary.pdf</AttachmentTitle>
32
<AttachmentDescription>Open preview dialog</AttachmentDescription>
33
</AttachmentContent>
34
<AttachmentActions>
35
<AttachmentAction aria-label="Copy link">
36
<CopyIcon />
37
</AttachmentAction>
38
<AttachmentAction aria-label="Remove research-summary.pdf">
39
<XIcon />
40
</AttachmentAction>
41
</AttachmentActions>
42
<AttachmentTrigger as={DialogTrigger} aria-label="Preview research-summary.pdf" />
43
</Attachment>
44
<DialogContent class="sm:max-w-md">
45
<DialogHeader>
46
<DialogTitle>research-summary.pdf</DialogTitle>
47
<DialogDescription>
48
The attachment trigger fills the card and opens the dialog, while the actions stay
49
independently clickable above it.
50
</DialogDescription>
51
</DialogHeader>
52
</DialogContent>
53
</Dialog>
54
</div>
55
);
56
}
1
<Dialog>
2
<Attachment>
3
{/* media, content, actions */}
4
<AttachmentTrigger as={DialogTrigger} aria-label="Preview research-summary.pdf" />
5
</Attachment>
6
<DialogContent>{/* ... */}</DialogContent>
7
</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.

1
<AttachmentAction aria-label="Remove sales-dashboard.pdf">
2
<XIcon />
3
</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.

1
<AttachmentTrigger
2
as="a"
3
href={url}
4
target="_blank"
5
rel="noreferrer"
6
aria-label="Open workspace.png"
7
/>

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.

PropTypeDefaultDescription
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.
classstring-Additional classes to apply to the root element.

AttachmentMedia

The media slot for an icon or image preview.

PropTypeDefaultDescription
variant"icon" | "image""icon"Whether the media holds an icon or an <img>.
classstring-Additional classes to apply to the media slot.

AttachmentContent

Wraps the title and description.

PropTypeDefaultDescription
classstring-Additional classes to apply to the content slot.

AttachmentTitle

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

PropTypeDefaultDescription
classstring-Additional classes to apply to the title.

AttachmentDescription

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

PropTypeDefaultDescription
classstring-Additional classes to apply to the description.

AttachmentActions

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

PropTypeDefaultDescription
classstring-Additional classes to apply to the actions.

AttachmentAction

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

PropTypeDefaultDescription
sizeButton["size"]"icon-xs"The button size.
...propsButtonProps-Props spread to the underlying Button.

AttachmentTrigger

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

PropTypeDefaultDescription
asValidComponent-Render as a different element, such as a link.
...propsComponentProps<"button">-Props spread to the trigger element.

AttachmentGroup

Lays out attachments in a horizontally scrollable, snapping row.

PropTypeDefaultDescription
classstring-Additional classes to apply to the group.

On This Page

  • Installation
  • Usage
  • Composition
  • Features
  • Image
  • States
  • Sizes
  • Group
  • Trigger
  • Accessibility
    • Label icon-only actions
    • Label the trigger
    • Keyboard scrolling
    • Meaning beyond color
  • API Reference
    • Attachment
    • AttachmentMedia
    • AttachmentContent
    • AttachmentTitle
    • AttachmentDescription
    • AttachmentActions
    • AttachmentAction
    • AttachmentTrigger
    • AttachmentGroup
Built by Kevin Abatan. The source code is available on GitHub.