---
id: "packages/active-ui/attachment"
title: "Attachment"
description: "A compact card for a file attached to a message or a form, with its state, a trigger to open it and actions."
url: "/packages/active-ui/attachment"
locale: "en"
revision: "bb81594b94841bd2"
---

# Attachment

An attachment shows one file as a small card: an icon or a thumbnail, the name, the size, and
actions such as remove. It shows the state of an upload too, from idle to done or failed. Several
attachments sit in a row that scrolls sideways. For picking files, see the composed
[`SimpleDropzone`](/packages/active-ui/simple-dropzone).

**Example: Two files, to open or remove**

Client: Runs entirely in your browser. Source: `examples/ui/attachment.tsx`.

```tsx
// apps/docs/examples/ui/attachment.tsx
'use client';

import {useState} from 'react';
import {FileSpreadsheetIcon, FileTextIcon, XIcon} from 'lucide-react';

import {formatByteSize} from '@workspace/active-format';
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentGroup,
  AttachmentMedia,
  AttachmentTitle,
  AttachmentTrigger,
} from '@workspace/active-ui/components/primitives/attachment';

const FILES = [
  {name: 'participants.csv', bytes: 48_213, icon: FileSpreadsheetIcon},
  {name: 'scheme-rules-2026.pdf', bytes: 1_204_331, icon: FileTextIcon},
];

/**
 * Files attached to a message or a form, as compact cards: an icon, the
 * name, the size (written by active-format), a trigger over the whole card
 * that opens the file, and an action that removes it.
 */
export default function AttachmentExample() {
  const [files, setFiles] = useState(FILES);
  const [opened, setOpened] = useState<string | null>(null);
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <AttachmentGroup>
        {files.map((file) => (
          <Attachment key={file.name}>
            <AttachmentTrigger
              aria-label={`Open ${file.name}`}
              onClick={() => setOpened(file.name)}
            />
            <AttachmentMedia>
              <file.icon aria-hidden />
            </AttachmentMedia>
            <AttachmentContent>
              <AttachmentTitle>{file.name}</AttachmentTitle>
              <AttachmentDescription>
                {formatByteSize(file.bytes, 'en', {system: 'decimal'})}
              </AttachmentDescription>
            </AttachmentContent>
            <AttachmentActions>
              <AttachmentAction
                aria-label={`Remove ${file.name}`}
                onClick={() =>
                  setFiles((current) =>
                    current.filter((other) => other.name !== file.name),
                  )
                }
              >
                <XIcon aria-hidden />
              </AttachmentAction>
            </AttachmentActions>
          </Attachment>
        ))}
      </AttachmentGroup>
      <p className="text-muted-foreground text-sm" aria-live="polite">
        {opened ? `Opened ${opened}` : `${files.length} files attached`}
      </p>
    </div>
  );
}
```

## Usage

```tsx
import {
  Attachment,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
  AttachmentTrigger,
} from '@workspace/active-ui/components/primitives/attachment';

<Attachment>
  <AttachmentTrigger aria-label="Open participants.csv" onClick={open} />
  <AttachmentMedia>
    <FileSpreadsheetIcon aria-hidden />
  </AttachmentMedia>
  <AttachmentContent>
    <AttachmentTitle>participants.csv</AttachmentTitle>
    <AttachmentDescription>48 kB</AttachmentDescription>
  </AttachmentContent>
</Attachment>;
```

## Anatomy

- `Attachment`: the card. `state`: `idle` (a dashed border), `uploading`, `processing`, `error`
  (a red tone) or `done` (default); `size`: `default`, `sm` or `xs`; `orientation`: `horizontal` or
  `vertical` (a tile with a large thumbnail).
- `AttachmentMedia`: the icon, or a picture with `variant="image"`, dimmed until the file is done.
- `AttachmentContent` with `AttachmentTitle` and `AttachmentDescription` (truncated).
- `AttachmentTrigger`: a button over the whole card, to open the file; give it an accessible name.
- `AttachmentActions` with `AttachmentAction`: buttons above the trigger, such as remove.
- `AttachmentGroup`: a row of attachments that scrolls sideways with snapping.

## What your application provides

- The file name, the size formatted with [active-format](/packages/active-format)
  (`formatByteSize`), and the state of an upload.
- An accessible name for the trigger and for each icon action, such as "Remove participants.csv".
- What opening and removing do.

## Accessibility and keyboard

- The trigger and each action are separate tab stops.
- A state shown only by colour (error) needs text too: say it in the description.

## API

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `orientation` | `"horizontal" \| "vertical" \| null` | no | `horizontal` is a row; `vertical` a tile with a large thumbnail. |
| `size` | `"default" \| "sm" \| "xs" \| null` | no | The padding and text size: `default`, `sm` or `xs`. |
| `state` | `"done" \| "error" \| "idle" \| "processing" \| "uploading"` | no | The state of the file: `idle` (not uploaded, a dashed border), `uploading`, `processing`, `error` (a red tone) or `done`. |

### Other parts

**AttachmentGroup**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

AttachmentGroup has no props of its own.

**AttachmentMedia**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `variant` | `"icon" \| "image" \| null` | no | `icon`, or `image` for a thumbnail, dimmed until the file is done. |

**AttachmentContent**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

AttachmentContent has no props of its own.

**AttachmentTitle**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

AttachmentTitle has no props of its own.

**AttachmentDescription**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

AttachmentDescription has no props of its own.

**AttachmentActions**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

AttachmentActions has no props of its own.

**AttachmentAction**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs" \| null` | no | The height and padding. `xs`, `sm`, `default` and `lg` for a button with text; `icon-xs`, `icon-sm`, `icon` and `icon-lg` for a square button with only an icon, which then needs an accessible name (`aria-label`). |
| `variant` | `"default" \| "destructive" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | no | The visual weight of the button. `default` for the main action of a screen or form; `outline` and `secondary` for other actions; `ghost` for a quiet action in a toolbar or a list; `destructive` for an action that deletes or cannot be undone; `link` for an action that reads as a link. Every variant but `link` sinks 1px while pressed; a link-styled action keeps its text and icon in place (`button-link-still`). |
| `focusableWhenDisabled` | `boolean` | no | Whether the button should be focusable when disabled. |
| `nativeButton` | `boolean` | no | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ComponentRenderFn<HTMLProps, ButtonState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**AttachmentTrigger**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `render` | `ComponentRenderFn<HTMLProps, {}> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |
