Components
- Accordion
- Alert
- Alert Dialog
- Ask User Questions
- Aspect Ratio
- Attachment
- Autocomplete
- Avatar
- Badge
- Bar List
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chat Message
- Checkbox
- Checkbox Group
- Circular Progress
- Collapsible
- Combobox
- Command
- Context Menu
- Copy Button
- Currency Input
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input
- Input Group
- Input Message
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Multi Combobox
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Phone Input
- Popover
- Progress
- Progress List
- Prompt
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Stat
- Switch
- Table
- Tabs
- Textarea
- Thinking Indicator
- Thinking Steps
- Toggle
- Toggle Group
- Tooltip
- Typography
Loading...
import { CodeIcon, XIcon } from "blode-icons-react";
import Image from "next/image";
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from "@/components/ui/attachment";
import { Spinner } from "@/components/ui/spinner";
const images = [
{
alt: "Workspace",
meta: "PNG · 820 KB",
name: "workspace.png",
src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
},
{
alt: "Desk",
meta: "JPG · 1.1 MB",
name: "desk-reference.jpg",
src: "https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80",
},
{
alt: "Office",
meta: "JPG · 940 KB",
name: "office-reference.jpg",
src: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80",
},
];
export 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">
<Image alt={image.alt} fill sizes="120px" src={image.src} />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>{image.name}</AttachmentTitle>
<AttachmentDescription>{image.meta}</AttachmentDescription>
</AttachmentContent>
</Attachment>
))}
</AttachmentGroup>
<Attachment className="w-full" state="uploading">
<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>
<CodeIcon />
</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>
);
}Requires shadcn ≥ 4.12.0 for the shimmer and scroll-fade utilities.
Installation
npx shadcn@latest add "https://ui.blode.co/r/styles/default/attachment"
Usage
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from "@/components/ui/attachment";<Attachment>
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>report.pdf</AttachmentTitle>
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove report.pdf">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>Examples
States
Set state to idle, uploading, processing, error, or done to reflect the upload lifecycle.
Loading...
import {
AlertTriangleIcon,
CheckIcon,
ClockIcon,
FileTextIcon,
RefreshCwIcon,
XIcon,
} from "blode-icons-react";
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from "@/components/ui/attachment";
import { Spinner } from "@/components/ui/spinner";
export function AttachmentStates() {
return (
<div className="mx-auto flex w-full max-w-sm flex-col gap-2 py-12">
<Attachment className="w-full" state="idle">
<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 className="w-full" state="uploading">
<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 className="w-full" state="processing">
<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 className="w-full" state="error">
<AttachmentMedia>
<AlertTriangleIcon />
</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 className="w-full" state="done">
<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 the size prop to switch between default, sm, and xs.
Loading...
import { FileTextIcon } from "blode-icons-react";
import {
Attachment,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from "@/components/ui/attachment";
export function AttachmentSizes() {
return (
<div className="mx-auto flex w-full max-w-sm flex-col gap-3 py-12">
<Attachment className="w-full" size="default">
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Default attachment</AttachmentTitle>
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
</AttachmentContent>
</Attachment>
<Attachment className="w-full" size="sm">
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Small attachment</AttachmentTitle>
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
</AttachmentContent>
</Attachment>
<Attachment className="w-full" size="xs">
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Extra small attachment</AttachmentTitle>
</AttachmentContent>
</Attachment>
</div>
);
}Image
Set variant="image" on AttachmentMedia to render an image preview.
Loading...
import { XIcon } from "blode-icons-react";
import Image from "next/image";
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
AttachmentTrigger,
} from "@/components/ui/attachment";
const images = [
{
alt: "Workspace",
meta: "PNG · 820 KB",
name: "workspace.png",
src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
},
{
alt: "Desk",
meta: "JPG · 1.1 MB",
name: "desk-reference.jpg",
src: "https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80",
},
{
alt: "Office",
meta: "JPG · 940 KB",
name: "office-reference.jpg",
src: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80",
},
];
export function AttachmentImage() {
return (
<div className="mx-auto w-full max-w-sm py-12">
<AttachmentGroup className="w-full">
{images.map((image) => (
<Attachment key={image.name} orientation="vertical">
<AttachmentMedia variant="image">
<Image alt={image.alt} fill sizes="120px" src={image.src} />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>{image.name}</AttachmentTitle>
<AttachmentDescription>{image.meta}</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label={`Remove ${image.name}`}>
<XIcon />
</AttachmentAction>
</AttachmentActions>
<AttachmentTrigger
render={
<a
aria-label={`Open ${image.name}`}
href={image.src}
rel="noreferrer"
target="_blank"
/>
}
/>
</Attachment>
))}
</AttachmentGroup>
</div>
);
}Group
Wrap attachments in AttachmentGroup for a horizontally scrollable row.
Loading...
import { CodeIcon, FileTextIcon, TableIcon, XIcon } from "blode-icons-react";
import Image from "next/image";
import type * as React from "react";
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from "@/components/ui/attachment";
interface Item {
name: string;
meta: string;
icon?: React.ComponentType<React.ComponentProps<"svg">>;
src?: string;
}
const items: Item[] = [
{ icon: FileTextIcon, meta: "PDF · 1.4 MB", name: "briefing-notes.pdf" },
{
meta: "PNG · 820 KB",
name: "workspace.png",
src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
},
{ icon: TableIcon, meta: "CSV · 18 KB", name: "customers.csv" },
{ icon: CodeIcon, meta: "TSX · 12 KB", name: "renderer.tsx" },
];
export function AttachmentGroupDemo() {
return (
<div className="mx-auto w-full max-w-sm py-12">
<AttachmentGroup className="w-full">
{items.map((item) => {
const Icon = item.icon;
return (
<Attachment className="w-64" key={item.name}>
{item.src ? (
<AttachmentMedia variant="image">
<Image alt={item.name} fill sizes="40px" src={item.src} />
</AttachmentMedia>
) : null}
{!item.src && 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
Use AttachmentTrigger to make the whole card open a dialog while actions stay clickable above it.
Loading...
import { CopyIcon, SearchIcon, XIcon } from "blode-icons-react";
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 function AttachmentTriggerDemo() {
return (
<div className="mx-auto w-full max-w-sm py-12">
<Dialog>
<Attachment className="w-full">
<AttachmentMedia>
<SearchIcon />
</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="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>
);
}