Attachment
Display file and image attachments with media, metadata, upload states, and actions.
'use client'
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from '@nerdfish/react/attachment'
import { Spinner } from '@nerdfish/react/spinner'
import { FileCodeIcon, XIcon } from 'lucide-react'
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 AttachmentExample() {
return (
<div className="gap-friends py-casual mx-auto flex w-full max-w-sm flex-col">
<AttachmentGroup>
{images.map((image) => (
<Attachment key={image.name} orientation="vertical">
<AttachmentMedia variant="image">
<img src={image.src} alt={image.alt} width={900} height={900} />
</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">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
<Attachment className="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>
)
}
Usage
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from '@nerdfish/react/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>
States
Set state to reflect the upload lifecycle. uploading and processing shimmer the title, and error switches to a destructive treatment.
'use client'
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from '@nerdfish/react/attachment'
import { Spinner } from '@nerdfish/react/spinner'
import {
CheckIcon,
ClockIcon,
FileTextIcon,
FileWarningIcon,
RefreshCwIcon,
XIcon,
} from 'lucide-react'
export default function AttachmentStatesExample() {
return (
<div className="gap-best-friends py-casual mx-auto flex w-full max-w-sm flex-col">
<Attachment state="idle" className="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" className="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" className="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" className="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" className="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.
'use client'
import {
Attachment,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from '@nerdfish/react/attachment'
import { FileTextIcon } from 'lucide-react'
export default function AttachmentSizesExample() {
return (
<div className="gap-friends py-casual mx-auto flex w-full max-w-sm flex-col">
<Attachment size="default" className="w-full">
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Default attachment</AttachmentTitle>
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
</AttachmentContent>
</Attachment>
<Attachment size="sm" className="w-full">
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Small attachment</AttachmentTitle>
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
</AttachmentContent>
</Attachment>
<Attachment size="xs" className="w-full">
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Extra small attachment</AttachmentTitle>
</AttachmentContent>
</Attachment>
</div>
)
}
Group
Wrap attachments in AttachmentGroup for a horizontally scrollable, snapping row with an edge fade.
'use client'
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from '@nerdfish/react/attachment'
import {
FileCodeIcon,
FileTextIcon,
TableIcon,
XIcon,
type LucideIcon,
} from 'lucide-react'
interface AttachmentItem {
name: string
meta: string
icon?: LucideIcon
src?: string
}
const items: AttachmentItem[] = [
{ 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 AttachmentGroupExample() {
return (
<div className="py-casual mx-auto w-full max-w-sm">
<AttachmentGroup className="w-full">
{items.map((item) => {
const Icon = item.icon
return (
<Attachment key={item.name} className="w-64">
{item.src ? (
<AttachmentMedia variant="image">
<img
src={item.src}
alt={item.name}
width={900}
height={900}
/>
</AttachmentMedia>
) : Icon ? (
<AttachmentMedia>
<Icon />
</AttachmentMedia>
) : null}
<AttachmentContent>
<AttachmentTitle>{item.name}</AttachmentTitle>
<AttachmentDescription>{item.meta}</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label={`Remove ${item.name}`}>
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
)
})}
</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.
'use client'
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
AttachmentTrigger,
} from '@nerdfish/react/attachment'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@nerdfish/react/dialog'
import { CopyIcon, FileSearchIcon, XIcon } from 'lucide-react'
export default function AttachmentTriggerExample() {
return (
<div className="py-casual mx-auto w-full max-w-sm">
<Dialog>
<Attachment className="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>
<DialogTrigger
render={
<AttachmentTrigger aria-label="Preview research-summary.pdf" />
}
/>
</Attachment>
<DialogContent className="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>
)
}