Skip to content
Nerdfishui
Esc
navigateopen⌘Jpreview
On this page

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>
	)
}