Attachment

File and image attachments with previews, states, and actions.

Show files in composers, threads, and upload lists. Each attachment can combine a preview, metadata, upload state, and actions.

lake-study.png
PNG · 820 KB
pine-ridge.png
PNG · 910 KB
coast-mist.png
PNG · 760 KB
registry-index.json
Uploading · 58%
accordion.tsx
TSX · 6.2 KB
tsx
import {  Attachment,  AttachmentAction,  AttachmentActions,  AttachmentContent,  AttachmentDescription,  AttachmentGroup,  AttachmentMedia,  AttachmentTitle,} from "@/components/ui/attachment"export function Example() {  return (    <div className="flex w-full flex-col gap-3">      <AttachmentGroup className="w-full sm:grid sm:grid-cols-3 sm:overflow-visible">        <Attachment orientation="vertical" className="w-52 shrink-0 snap-start snap-always sm:w-full sm:snap-align-none">          <AttachmentMedia variant="image">            <img src="/assets/brand/demos/attachments/attachment-1.png" alt="" />          </AttachmentMedia>          <AttachmentContent>            <AttachmentTitle>lake-study.png</AttachmentTitle>            <AttachmentDescription>PNG · 820 KB</AttachmentDescription>          </AttachmentContent>        </Attachment>        {/* …two more image attachments */}      </AttachmentGroup>      <Attachment state="uploading">        <AttachmentMedia>          <FileTextIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>registry-index.json</AttachmentTitle>          <AttachmentDescription>Uploading · 58%</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove registry-index.json">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>      <Attachment>        <AttachmentMedia>          <FileCodeIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>accordion.tsx</AttachmentTitle>          <AttachmentDescription>TSX · 6.2 KB</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove accordion.tsx">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>    </div>  )}

Installation

npx @arctis-sh/@arctis-sh/ui@latest add attachment

Usage

import {  Attachment,  AttachmentAction,  AttachmentActions,  AttachmentContent,  AttachmentDescription,  AttachmentMedia,  AttachmentTitle,} from "@/components/ui/attachment"
<Attachment>  <AttachmentMedia>    <FileTextIcon />  </AttachmentMedia>  <AttachmentContent>    <AttachmentTitle>registry-index.json</AttachmentTitle>    <AttachmentDescription>JSON · 48 KB</AttachmentDescription>  </AttachmentContent>  <AttachmentActions>    <AttachmentAction aria-label="Remove registry-index.json">      <XIcon />    </AttachmentAction>  </AttachmentActions></Attachment>

Composition

Attachment
├── AttachmentMedia
├── AttachmentContent
│   ├── AttachmentTitle
│   └── AttachmentDescription
├── AttachmentActions
│   └── AttachmentAction
└── AttachmentTrigger

AttachmentGroup
├── Attachment
└── Attachment

Basic

Mix image previews and compact file attachments. Set state="uploading" while a file is in progress.

lake-study.png
PNG · 820 KB
pine-ridge.png
PNG · 910 KB
coast-mist.png
PNG · 760 KB
registry-index.json
Uploading · 58%
accordion.tsx
TSX · 6.2 KB
tsx
import {  Attachment,  AttachmentAction,  AttachmentActions,  AttachmentContent,  AttachmentDescription,  AttachmentGroup,  AttachmentMedia,  AttachmentTitle,} from "@/components/ui/attachment"export function Example() {  return (    <div className="flex w-full flex-col gap-3">      <AttachmentGroup className="w-full sm:grid sm:grid-cols-3 sm:overflow-visible">        <Attachment orientation="vertical" className="w-52 shrink-0 snap-start snap-always sm:w-full sm:snap-align-none">          <AttachmentMedia variant="image">            <img src="/assets/brand/demos/attachments/attachment-1.png" alt="" />          </AttachmentMedia>          <AttachmentContent>            <AttachmentTitle>lake-study.png</AttachmentTitle>            <AttachmentDescription>PNG · 820 KB</AttachmentDescription>          </AttachmentContent>        </Attachment>        {/* …two more image attachments */}      </AttachmentGroup>      <Attachment state="uploading">        <AttachmentMedia>          <FileTextIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>registry-index.json</AttachmentTitle>          <AttachmentDescription>Uploading · 58%</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove registry-index.json">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>      <Attachment>        <AttachmentMedia>          <FileCodeIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>accordion.tsx</AttachmentTitle>          <AttachmentDescription>TSX · 6.2 KB</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove accordion.tsx">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>    </div>  )}

Image

Set variant="image" on AttachmentMedia and orientation="vertical" to place a preview above the file name and metadata.

lake-study.png
PNG · 820 KB
pine-ridge.png
PNG · 910 KB
coast-mist.png
PNG · 760 KB
tsx
import {  Attachment,  AttachmentContent,  AttachmentDescription,  AttachmentGroup,  AttachmentMedia,  AttachmentTitle,} from "@/components/ui/attachment"export function Example() {  return (    <AttachmentGroup className="w-full sm:grid sm:grid-cols-3 sm:overflow-visible">      <Attachment orientation="vertical" className="w-52 shrink-0 snap-start snap-always sm:w-full sm:snap-align-none">        <AttachmentMedia variant="image">          <img src="/assets/brand/demos/attachments/attachment-1.png" alt="" />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>lake-study.png</AttachmentTitle>          <AttachmentDescription>PNG · 820 KB</AttachmentDescription>        </AttachmentContent>      </Attachment>    </AttachmentGroup>  )}

States

state controls the visual treatment. uploading and processing dim the media and show progress, while error uses a destructive surface.

theme-tokens.css
Queued · waiting to upload
docs-export.md
Uploading · 58%
component-catalog.json
Indexing for the registry
og-cover.png
Upload failed · try again
alert-dialog.tsx
Uploaded · 9.4 KB
tsx
import {  Attachment,  AttachmentAction,  AttachmentActions,  AttachmentContent,  AttachmentDescription,  AttachmentMedia,  AttachmentTitle,} from "@/components/ui/attachment"export function Example() {  return (    <div className="flex flex-col gap-3">      <Attachment state="idle">        <AttachmentMedia>          <ClockIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>theme-tokens.css</AttachmentTitle>          <AttachmentDescription>Queued · waiting to upload</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove theme-tokens.css">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>      <Attachment state="uploading">        <AttachmentMedia>          <FileTextIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>docs-export.md</AttachmentTitle>          <AttachmentDescription>Uploading · 58%</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove docs-export.md">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>      <Attachment state="processing">        <AttachmentMedia>          <FileTextIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>component-catalog.json</AttachmentTitle>          <AttachmentDescription>Indexing for the registry</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove component-catalog.json">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>      <Attachment state="error">        <AttachmentMedia>          <AlertIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>og-cover.png</AttachmentTitle>          <AttachmentDescription>Upload failed · try again</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Retry og-cover.png">            <RetryIcon />          </AttachmentAction>          <AttachmentAction aria-label="Remove og-cover.png">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>      <Attachment state="done">        <AttachmentMedia>          <CheckIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>alert-dialog.tsx</AttachmentTitle>          <AttachmentDescription>Uploaded · 9.4 KB</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove alert-dialog.tsx">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>    </div>  )}

Sizes

Choose default, sm, or xs with the size prop.

Default size
MD · 2.1 KB
Small size
MD · 2.1 KB
Extra small
MD · 2.1 KB
tsx
import {  Attachment,  AttachmentContent,  AttachmentDescription,  AttachmentMedia,  AttachmentTitle,} from "@/components/ui/attachment"export function Example() {  return (    <div className="flex flex-col gap-3">      <Attachment size="default">        <AttachmentMedia>          <FileTextIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>Default size</AttachmentTitle>          <AttachmentDescription>MD · 2.1 KB</AttachmentDescription>        </AttachmentContent>      </Attachment>      <Attachment size="sm">        <AttachmentMedia>          <FileTextIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>Small size</AttachmentTitle>          <AttachmentDescription>MD · 2.1 KB</AttachmentDescription>        </AttachmentContent>      </Attachment>      <Attachment size="xs">        <AttachmentMedia>          <FileTextIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>Extra small</AttachmentTitle>          <AttachmentDescription>MD · 2.1 KB</AttachmentDescription>        </AttachmentContent>      </Attachment>    </div>  )}

Group

AttachmentGroup arranges items in a horizontal row. Swipe on touch or drag on desktop; the row settles on the nearest card.

dune-pass.png
PNG · 640 KB
release-notes.md
MD · 12 KB
alpine.png
PNG · 700 KB
button.tsx
TSX · 4 KB
tsx
import {  Attachment,  AttachmentAction,  AttachmentActions,  AttachmentContent,  AttachmentDescription,  AttachmentGroup,  AttachmentMedia,  AttachmentTitle,} from "@/components/ui/attachment"export function Example() {  return (    <AttachmentGroup>      <Attachment className="w-64 shrink-0">        <AttachmentMedia variant="image">          <img src="/assets/brand/demos/attachments/attachment-4.png" alt="" />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>dune-pass.png</AttachmentTitle>          <AttachmentDescription>PNG · 640 KB</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove dune-pass.png">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>      <Attachment className="w-64 shrink-0">        <AttachmentMedia>          <FileTextIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>release-notes.md</AttachmentTitle>          <AttachmentDescription>MD · 12 KB</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove release-notes.md">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>      <Attachment className="w-64 shrink-0">        <AttachmentMedia variant="image">          <img src="/assets/brand/demos/attachments/attachment-5.png" alt="" />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>alpine.png</AttachmentTitle>          <AttachmentDescription>PNG · 700 KB</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove alpine.png">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>      <Attachment className="w-64 shrink-0">        <AttachmentMedia>          <FileCodeIcon />        </AttachmentMedia>        <AttachmentContent>          <AttachmentTitle>button.tsx</AttachmentTitle>          <AttachmentDescription>TSX · 4 KB</AttachmentDescription>        </AttachmentContent>        <AttachmentActions>          <AttachmentAction aria-label="Remove button.tsx">            <XIcon />          </AttachmentAction>        </AttachmentActions>      </Attachment>    </AttachmentGroup>  )}

Trigger

AttachmentTrigger fills the card, making the whole attachment clickable while keeping its actions available.

changelog.md
Open the full notes
tsx
import {  Attachment,  AttachmentAction,  AttachmentActions,  AttachmentContent,  AttachmentDescription,  AttachmentMedia,  AttachmentTitle,  AttachmentTrigger,} from "@/components/ui/attachment"export function Example() {  return (    <Attachment>      <AttachmentMedia>        <FileTextIcon />      </AttachmentMedia>      <AttachmentContent>        <AttachmentTitle>changelog.md</AttachmentTitle>        <AttachmentDescription>Open the full notes</AttachmentDescription>      </AttachmentContent>      <AttachmentActions>        <AttachmentAction aria-label="Download changelog.md">          <DownloadIcon />        </AttachmentAction>        <AttachmentAction aria-label="Remove changelog.md">          <XIcon />        </AttachmentAction>      </AttachmentActions>      <AttachmentTrigger aria-label="Open changelog.md" />    </Attachment>  )}

API Reference

Attachment

PropTypeDefault
state"idle" | "uploading" | "processing" | "error" | "done""done"
size"default" | "sm" | "xs""default"
orientation"horizontal" | "vertical""horizontal"
classNamestring
<Attachment>  <AttachmentMedia>    <Icon />  </AttachmentMedia>  <AttachmentContent>    <AttachmentTitle>report.pdf</AttachmentTitle>    <AttachmentDescription>2.4 MB</AttachmentDescription>  </AttachmentContent></Attachment>

AttachmentMedia

PropTypeDefault
variant"icon" | "image""icon"
classNamestring
<AttachmentMedia>  <Icon /></AttachmentMedia>

AttachmentContent

PropTypeDefault
classNamestring
<AttachmentContent>  <AttachmentTitle>report.pdf</AttachmentTitle>  <AttachmentDescription>2.4 MB</AttachmentDescription></AttachmentContent>

AttachmentTitle

PropTypeDefault
classNamestring
<AttachmentTitle>report.pdf</AttachmentTitle>

AttachmentDescription

PropTypeDefault
classNamestring
<AttachmentDescription>2.4 MB</AttachmentDescription>

AttachmentActions

PropTypeDefault
classNamestring
<AttachmentActions>  <AttachmentAction aria-label="Remove" /></AttachmentActions>

AttachmentAction

PropTypeDefault
classNamestring
aria-labelstring
<AttachmentAction aria-label="Download" />

AttachmentTrigger

PropTypeDefault
classNamestring
aria-labelstring
<AttachmentTrigger>  <Attachment>...</Attachment></AttachmentTrigger>

AttachmentGroup

PropTypeDefault
classNamestring
<AttachmentGroup>  <Attachment>...</Attachment>  <Attachment>...</Attachment></AttachmentGroup>