Attachment
Menampilkan lampiran berupa berkas atau gambar, lengkap dengan media, metadata, status unggah, dan aksinya.
<script setup lang="ts">
import { FileCodeIcon, XIcon } from '@lucide/vue'
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',
},
]
</script>
<template>
<div class="mx-auto flex w-full max-w-sm flex-col gap-3 py-12">
<AttachmentGroup>
<template v-for="image in images" :key="image.name">
<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>
</template>
</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.vue</AttachmentTitle>
<AttachmentDescription>TypeScript · 12 KB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove message-renderer.vue">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
</div>
</template>Komponen Attachment menampilkan lampiran berupa berkas atau gambar beserta media, nama, dan metadatanya, ditambah aksi dan status unggah kalau diperlukan. Cocok untuk berkas dan gambar di kolom penulisan chat, utas pesan, dan daftar unggahan.
Instalasi
CLI
npx shadcn-vue@latest add attachmentManual
Salin dan tempel kode sumber di GitHub ke project Anda.
Sesuaikan path import dengan struktur project Anda.
Penggunaan
<script setup lang="ts">
import { FileTextIcon, XIcon } from '@lucide/vue'
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from '@/components/ui/attachment'
</script>
<template>
<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>
</template>Komposisi
Susunan berikut adalah cara membangun sebuah attachment:
Attachment
├── AttachmentMedia
├── AttachmentContent
│ ├── AttachmentTitle
│ └── AttachmentDescription
├── AttachmentActions
│ └── AttachmentAction
└── AttachmentTriggerPakai AttachmentGroup untuk menata beberapa lampiran dalam satu baris yang bisa digulir:
AttachmentGroup
├── Attachment
└── AttachmentFitur
- Media berupa ikon maupun gambar lewat
AttachmentMedia. - Status unggah
idle,uploading,processing,error, dandone, lengkap dengan style bawaan dan efek shimmer selagi berjalan. - Tiga pilihan ukuran, dengan orientasi mendatar atau tegak.
AttachmentTriggerseluas kartu yang membuka tautan atau dialog, tanpa mengganggu tombol aksi yang tetap bisa diklik sendiri.AttachmentGroupyang bisa digulir dengan efek snap dan pudar di tepinya.- Style tiap bagian bisa disesuaikan lewat atribut
classVue.
Contoh
Image
Isi variant="image" pada AttachmentMedia lalu taruh <img> di dalamnya. Pakai orientation="vertical" kalau medianya ingin ditumpuk di atas konten.
<script setup lang="ts">
import { XIcon } from '@lucide/vue'
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',
},
]
</script>
<template>
<div class="mx-auto w-full max-w-sm py-12">
<AttachmentGroup class="w-full">
<template v-for="image in images" :key="image.name">
<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-child
>
<a
:href="image.src"
target="_blank"
rel="noreferrer"
:aria-label="`Open ${image.name}`"
/>
</AttachmentTrigger>
</Attachment>
</template>
</AttachmentGroup>
</div>
</template>Status
Isi state sesuai tahapan unggahnya. uploading dan processing membuat judulnya berkilat, sedangkan error mengubah tampilannya jadi bernuansa peringatan.
<script setup lang="ts">
import { CheckIcon, ClockIcon, FileTextIcon, FileWarningIcon, RefreshCwIcon, XIcon } from '@lucide/vue'
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from '@/components/ui/attachment'
import { Spinner } from '@/components/ui/spinner'
</script>
<template>
<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>
</template>Ukuran
Pakai size untuk berpindah antara default, sm, dan xs.
<script setup lang="ts">
import { FileTextIcon } from '@lucide/vue'
import {
Attachment,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from '@/components/ui/attachment'
</script>
<template>
<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>
</template>Grup
Bungkus beberapa lampiran dengan AttachmentGroup untuk menatanya dalam satu baris mendatar yang bisa digulir, dengan efek snap dan pudar di tepinya.
<script setup lang="ts">
import type { LucideIcon } from '@lucide/vue'
import { FileCodeIcon, FileTextIcon, TableIcon, XIcon } from '@lucide/vue'
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from '@/components/ui/attachment'
interface Item {
name: string
meta: string
icon?: LucideIcon
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.vue', meta: 'Vue · 12 KB', icon: FileCodeIcon },
]
</script>
<template>
<div class="mx-auto w-full max-w-sm py-12">
<AttachmentGroup class="w-full">
<template v-for="item in items" :key="item.name">
<Attachment class="w-64">
<AttachmentMedia v-if="item.src" variant="image">
<img :src="item.src" :alt="item.name">
</AttachmentMedia>
<AttachmentMedia v-if="item.icon">
<component :is="item.icon" />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>{{ item.name }}</AttachmentTitle>
<AttachmentDescription>{{ item.meta }}</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction :aria-label="`Remove ${item.name}`">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
</template>
</AttachmentGroup>
</div>
</template>Trigger
Tambahkan AttachmentTrigger supaya seluruh kartu bisa membuka tautan atau dialog. Ia mengisi kartu di belakang tombol aksi, jadi tombolnya tetap bisa diklik.
<script setup lang="ts">
import { CopyIcon, FileSearchIcon, XIcon } from '@lucide/vue'
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
AttachmentTrigger,
} from '@/components/ui/attachment'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
</script>
<template>
<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>
<DialogTrigger as-child>
<AttachmentTrigger aria-label="Preview research-summary.pdf" />
</DialogTrigger>
</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>
</template><template>
<Dialog>
<Attachment>
<!-- media, content, actions -->
<DialogTrigger as-child>
<AttachmentTrigger aria-label="Preview research-summary.pdf" />
</DialogTrigger>
</Attachment>
<DialogContent>
<!-- ... -->
</DialogContent>
</Dialog>
</template>Aksesibilitas
AttachmentAction menghasilkan Button, sedangkan AttachmentTrigger menghasilkan <button> sungguhan (atau elemen Anda sendiri lewat as-child). Ikuti panduan di bawah agar keduanya bisa dioperasikan dan dibacakan dengan benar.
Beri label pada aksi yang hanya berupa ikon
AttachmentAction biasanya hanya berupa ikon, jadi berilah masing-masing aria-label yang menjelaskan aksinya dan sasarannya.
<template>
<AttachmentAction aria-label="Remove sales-dashboard.pdf">
<XIcon />
</AttachmentAction>
</template>Beri label pada trigger
AttachmentTrigger menutupi seluruh kartu tanpa teks sendiri, jadi berilah aria-label yang menjelaskan apa yang terjadi saat ia diaktifkan.
<template>
<AttachmentTrigger as-child>
<a
:href="url"
target="_blank"
rel="noreferrer"
aria-label="Open workspace.png"
/>
</AttachmentTrigger>
</template>Trigger-nya berada di belakang tombol aksi dalam urutan tumpukan, jadi AttachmentAction dan AttachmentTrigger tidak saling menghalangi — keduanya tetap bisa di-focus dan diklik secara terpisah.
Menggulir lewat keyboard
AttachmentGroup bergulir mendatar. Kalau lampirannya interaktif — punya trigger atau tombol aksi — pengguna keyboard bisa menjangkau item di luar layar lewat tombol Tab. Untuk deretan lampiran yang hanya bersifat tampilan, buat grupnya sendiri bisa di-focus dan digulir dengan menambahkan tabindex="0", role="group", dan aria-label.
Makna yang tidak bergantung warna
Status error memakai warna peringatan. Cantumkan alasan kegagalannya di AttachmentDescription supaya maknanya tidak bergantung pada warna semata.
Referensi API
Attachment
Wadah terluar attachment.
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
class | HTMLAttributes["class"] | - | Additional classes to apply to the root element. |
AttachmentMedia
Slot media untuk ikon atau pratinjau gambar.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "icon" | "image" | "icon" | Whether the media holds an icon or an <img>. |
class | HTMLAttributes["class"] | - | Additional classes to apply to the media slot. |
AttachmentContent
Membungkus judul dan keterangan.
| Prop | Tipe | Bawaan | Description |
|---|---|---|---|
class | HTMLAttributes["class"] | - | Additional classes to apply to the content slot. |
AttachmentTitle
Nama lampirannya. Berkilat selagi statusnya uploading atau processing.
| Prop | Tipe | Bawaan | Description |
|---|---|---|---|
class | HTMLAttributes["class"] | - | Additional classes to apply to the title. |
AttachmentDescription
Metadata tambahan seperti tipe berkas, ukuran, atau status unggah.
| Prop | Tipe | Bawaan | Description |
|---|---|---|---|
class | HTMLAttributes["class"] | - | Additional classes to apply to the description. |
AttachmentActions
Wadah untuk satu atau beberapa aksi, diratakan ke sisi akhir lampiran.
| Prop | Tipe | Bawaan | Description |
|---|---|---|---|
class | HTMLAttributes["class"] | - | Additional classes to apply to the actions. |
AttachmentAction
Tombol aksi. Menghasilkan Button dan menerima atribut Vue yang diteruskan, misalnya aria-label.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | ButtonVariants["variant"] | "ghost" | The button variant. |
size | ButtonVariants["size"] | "icon-xs" | The button size. |
class | HTMLAttributes["class"] | - | Additional classes to apply to the action. |
AttachmentTrigger
Lapisan seluas kartu yang mengaktifkan lampiran. Menghasilkan <button> secara bawaan dan menerima atribut Vue yang diteruskan, misalnya aria-label.
| Prop | Type | Default | Description |
|---|---|---|---|
as | PrimitiveProps["as"] | "button" | Element or component to render. |
as-child | boolean | false | Render as the child element, such as a link. |
class | HTMLAttributes["class"] | - | Additional classes to apply to the trigger. |
AttachmentGroup
Menata lampiran dalam satu baris mendatar yang bisa digulir dengan efek snap.
| Prop | Tipe | Bawaan | Description |
|---|---|---|---|
class | HTMLAttributes["class"] | - | Additional classes to apply to the group. |