# AttachmentRow

One attached file — a preview, the name, a line of meta, the stage its upload is at, and the actions that act on it.

```slint
import { AttachmentRow, AttachmentState } from "@glint/components/attachment-row.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 460px;
    height: 220px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        spacing: 12px;
        alignment: center;

        AttachmentRow {
            icon: IconSet.FileText;
            name: "release-notes.md";
            meta: "Markdown · 12 KB";
            removable: true;
            remove-label: "Remove release-notes.md";
        }

        AttachmentRow {
            icon: IconSet.Image;
            name: "screenshot.png";
            meta: "Uploading — 40%";
            state: AttachmentState.uploading;
            progress: 0.4;
        }
    }
}
```

## Usage

```slint
import { AttachmentRow, AttachmentState } from "@glint/components/attachment-row.slint";
import { IconSet } from "@lucide";

export component AppWindow inherits Window {
    in property <float> sent;

    VerticalLayout {
        AttachmentRow {
            icon: IconSet.FileText;
            name: "report.pdf";
            meta: "PDF · 240 KB";
            state: AttachmentState.uploading;
            progress: root.sent;
            removable: true;
            // An icon-only control is a nameless button without this.
            remove-label: "Remove report.pdf";
            removed => { /* drop it from your model */ }
        }
    }
}
```

**The row is standalone-renderable.** It carries its own surface and reads nothing from its container, so it looks the same inside a [message bubble](/docs/components/message-bubble), in a composer, or on a plain page. That surface is built from the alpha-based overlay tokens, which composite over whatever background the container happens to have instead of fighting it with an opaque fill.

`state` is the upload’s lifecycle and every value has a treatment of its own — **and none of them is only a color.** A working row carries a [Progress](/docs/components/progress) the accessibility tree can read; a failed one wears the destructive edge *and* says why on its `meta` line, because a color is a second channel and never the only one.

The row publishes what a call site would otherwise have to restate: `media-size` for a gutter to line up against, `working` for the two states that are a transfer, and `edge-color`, `surface-color` and `meta-color` for a slotted action that wants to match the row it is sitting on.

## Examples

### Upload states

`idle` is the empty slot before anything was sent. `uploading` shows how far the transfer has got, `processing` has no number to give and spins instead, `error` says why in words and wears the destructive edge, and `done` — the default — is a file that is simply there.

```slint
import { AttachmentRow, AttachmentState } from "@glint/components/attachment-row.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 460px;
    height: 380px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        spacing: 10px;
        alignment: center;

        AttachmentRow {
            icon: IconSet.Paperclip;
            name: "Attach a file";
            meta: "Nothing sent yet";
            state: AttachmentState.idle;
        }
        AttachmentRow {
            icon: IconSet.Image;
            name: "diagram.png";
            meta: "Uploading — 65%";
            state: AttachmentState.uploading;
            progress: 0.65;
        }
        AttachmentRow {
            icon: IconSet.FileText;
            name: "transcript.txt";
            meta: "Extracting the text";
            state: AttachmentState.processing;
        }
        AttachmentRow {
            icon: IconSet.FileText;
            name: "budget.xlsx";
            // The reason is on the meta line, never in the edge alone.
            meta: "Upload failed — the file is larger than 25 MB";
            state: AttachmentState.error;
        }
        AttachmentRow {
            icon: IconSet.FileText;
            name: "notes.md";
            meta: "Markdown · 4 KB";
            state: AttachmentState.done;
        }
    }
}
```

### Sizes

`size` compresses the media square, the padding and both type sizes together. `xs` is the inline chip a turn mentions a file with, `sm` the dense composer strip, `default` the row on its own.

```slint
import { AttachmentRow, AttachmentSize } from "@glint/components/attachment-row.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 460px;
    height: 260px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        spacing: 12px;
        alignment: center;

        AttachmentRow {
            size: AttachmentSize.xs;
            icon: IconSet.FileText;
            name: "notes.md";
        }
        AttachmentRow {
            size: AttachmentSize.sm;
            icon: IconSet.FileText;
            name: "notes.md";
            meta: "4 KB";
        }
        AttachmentRow {
            size: AttachmentSize.default;
            icon: IconSet.FileText;
            name: "notes.md";
            meta: "Markdown · 4 KB";
        }
    }
}
```

### The card

`orientation: vertical` puts a large preview above the file name — the only shape an image attachment reads in, since a 32px square can show a thumbnail but never a legible preview. `preview-height` is how tall that preview is.

The axis stays a property rather than becoming a second component because the actions are *placed* rather than laid out (ADR-0035): the slot rides the trailing corner of whichever shape the row is — the trailing edge of a row, the top corner of a card — so nothing conditional ever contains it.

```slint
import { AttachmentOrientation, AttachmentRow } from "@glint/components/attachment-row.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 460px;
    height: 300px;
    background: Tokens.color-background;

    HorizontalLayout {
        padding: 24px;
        spacing: 16px;
        alignment: center;

        VerticalLayout {
            alignment: center;
            AttachmentRow {
                width: 190px;
                orientation: AttachmentOrientation.horizontal;
                icon: IconSet.Image;
                name: "cover.png";
                meta: "PNG · 1.2 MB";
                removable: true;
                remove-label: "Remove cover.png";
            }
        }

        VerticalLayout {
            alignment: center;
            AttachmentRow {
                width: 190px;
                orientation: AttachmentOrientation.vertical;
                preview-height: 110px;
                icon: IconSet.Image;
                name: "cover.png";
                meta: "PNG · 1.2 MB";
                removable: true;
                remove-label: "Remove cover.png";
            }
        }
    }
}
```

### Actions

The one `@children` is the actions: any number of them, sitting before the remove control, which stays the trailing one — so the destructive action is always in the same place. `removable` is what draws it, and `remove-label` is what assistive technology addresses it by, because an icon-only control is a nameless button without one.

```slint
import { AttachmentRow, AttachmentState } from "@glint/components/attachment-row.slint";
import { Button, ButtonSize, ButtonVariant } from "@glint/components/button.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 460px;
    height: 240px;
    background: Tokens.color-background;

    in-out property <string> last: "Nothing taken yet.";

    VerticalLayout {
        padding: 24px;
        spacing: 12px;
        alignment: center;

        AttachmentRow {
            icon: IconSet.FileText;
            name: "backup.tar.gz";
            meta: "Upload failed — the connection dropped";
            state: AttachmentState.error;
            removable: true;
            remove-label: "Remove backup.tar.gz";
            removed => { root.last = "Removed backup.tar.gz."; }

            Button {
                variant: ButtonVariant.ghost;
                size: ButtonSize.icon-sm;
                accessible-label: "Retry the upload";
                leading-icon: IconSet.RotateCw;
                clicked => { root.last = "Retrying the upload."; }
            }
        }

        Text {
            text: root.last;
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
        }
    }
}
```

### A row that opens the file

`interactive` makes the whole row a control — opening or previewing the file. Its control layer sits *under* everything, so a press the actions did not want falls through to it and one they did never reaches it: the trigger and the actions cannot trap each other. `action-label` is what the row is announced as, defaulting to `name`.

```slint
import { AttachmentRow } from "@glint/components/attachment-row.slint";
import { Button, ButtonSize, ButtonVariant } from "@glint/components/button.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 460px;
    height: 200px;
    background: Tokens.color-background;

    in-out property <string> last: "Nothing taken yet.";

    VerticalLayout {
        padding: 24px;
        spacing: 12px;
        alignment: center;

        AttachmentRow {
            icon: IconSet.FileText;
            name: "spec.pdf";
            meta: "PDF · 88 KB";
            interactive: true;
            action-label: "Open spec.pdf";
            clicked => { root.last = "Opened spec.pdf."; }

            Button {
                variant: ButtonVariant.ghost;
                size: ButtonSize.icon-sm;
                accessible-label: "Download spec.pdf";
                leading-icon: IconSet.Download;
                clicked => { root.last = "Downloaded spec.pdf — the row was not opened."; }
            }
        }

        Text {
            text: root.last;
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
        }
    }
}
```

### AttachmentGroup — the composer strip

Several attachments sit in one named group that scrolls sideways: the composer’s attachment tray. It is a group rather than a list because its items are files you put there, not rows of a model it owns — so it takes them through `@children`, and `label` is the name everything in it is announced under.

The scrolling is the library’s one surface, so an attachment past the edge is reachable from the keyboard for free. `content-takes-focus` is the one thing the strip cannot answer for itself: Slint’s tab walk is pre-order, so the strip’s own stop lands *in front of* whatever its rows own. Leave it false and the strip takes the stop and the arrows scroll it, which is right for inert rows; set it true when the rows are live and the extra stop in front of them is not worth its cost.

```slint
import { AttachmentGroup, AttachmentRow, AttachmentSize } from "@glint/components/attachment-row.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 460px;
    height: 200px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        alignment: center;

        AttachmentGroup {
            label: "Attachments on this message";

            AttachmentRow {
                width: 180px;
                size: AttachmentSize.sm;
                icon: IconSet.FileText;
                name: "notes.md";
                meta: "4 KB";
            }
            AttachmentRow {
                width: 180px;
                size: AttachmentSize.sm;
                icon: IconSet.Image;
                name: "diagram.png";
                meta: "1.2 MB";
            }
            AttachmentRow {
                width: 180px;
                size: AttachmentSize.sm;
                icon: IconSet.FileText;
                name: "budget.xlsx";
                meta: "36 KB";
            }
        }
    }
}
```

## API Reference

### Properties

| Property         | Type                       | Default                            | Description                                                                                                                                                                                            |
| ---------------- | -------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`           | `in string`                | no default                         | File name, shown on the first line; elides when the row is too narrow.                                                                                                                                 |
| `meta`           | `in string`                | no default                         | Secondary line — size, type, and the reason an upload failed. A failed upload has to say why in words: the destructive edge is a second channel, never the only one.                                   |
| `icon`           | `in LucideIcon`            | `IconSet.Paperclip`                | Leading icon, normally selected from `IconSet` in lucide-slint. Ignored while `thumbnail` holds an image.                                                                                              |
| `thumbnail`      | `in image`                 | no default                         | Preview image; when set it replaces the icon in the media square, and fills the preview of a vertical card.                                                                                            |
| `removable`      | `in bool`                  | `false`                            | Show the trailing remove action.                                                                                                                                                                       |
| `remove-label`   | `in string`                | `@tr("Remove")`                    | Name assistive technology announces for the remove action. The control is icon-only, so without a name it reads as an anonymous button — override it to translate it, or to say what is being removed. |
| `state`          | `in AttachmentState`       | `AttachmentState.done`             | Where the upload stands.                                                                                                                                                                               |
| `progress`       | `in float`                 | `0.0`                              | How far a transfer has got, `0..1`. Read only while `state` is `uploading`; `processing` has no number to show and spins instead.                                                                      |
| `size`           | `in AttachmentSize`        | `AttachmentSize.default`           | Density.                                                                                                                                                                                               |
| `orientation`    | `in AttachmentOrientation` | `AttachmentOrientation.horizontal` | Row or card.                                                                                                                                                                                           |
| `preview-height` | `in length`                | `120px`                            | How tall a card's preview is. Ignored by the horizontal row, whose media is the `media-size` square.                                                                                                   |
| `interactive`    | `in bool`                  | `false`                            | Makes the whole row activatable — opening or previewing the file. The actions keep their own presses; the row's layer sits behind them.                                                                |
| `action-label`   | `in string`                | `root.name`                        | What assistive technology announces the row as while it is a control.                                                                                                                                  |
| `media-size`     | `out length`               | no default                         | The leading square's side, published because a call site laying several rows out against a gutter needs the same number.                                                                               |
| `working`        | `out bool`                 | no default                         | Whether a transfer is under way — what puts a Progress on the row.                                                                                                                                     |
| `edge-color`     | `out color`                | no default                         | The row's edge, for slotted actions that want to match it.                                                                                                                                             |
| `surface-color`  | `out color`                | no default                         | The surface behind the row. An idle attachment is the emptier slot.                                                                                                                                    |
| `meta-color`     | `out color`                | no default                         | The meta line's colour — destructive while the reason it carries is a failure.                                                                                                                         |

### Callbacks

| Callback    | Description                                                                                                         |
| ----------- | ------------------------------------------------------------------------------------------------------------------- |
| `removed()` | Fired when the remove action is activated by pointer or keyboard.                                                   |
| `clicked()` | Fired when an interactive row is activated by pointer, by Enter or Space, or through the accessible default action. |

### Enums

| Enum                    | Values                                             |
| ----------------------- | -------------------------------------------------- |
| `AttachmentState`       | `idle`, `uploading`, `processing`, `error`, `done` |
| `AttachmentSize`        | `xs`, `sm`, `default`                              |
| `AttachmentOrientation` | `horizontal`, `vertical`                           |

### AttachmentGroup

The strip several attachments sit in — a labeled group that scrolls its rows sideways.

### Properties

| Property              | Type        | Default              | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| --------------------- | ----------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label`               | `in string` | `@tr("Attachments")` | What the group is called. Everything in it is announced under this name, so a composer with two trays can tell them apart.                                                                                                                                                                                                                                                                                                                                                                                                    |
| `spacing`             | `in length` | `Tokens.spacing-sm`  | Gap between two attachments.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `content-takes-focus` | `in bool`   | `false`              | Whether the rows slotted in here answer the keyboard themselves — an `interactive` AttachmentRow does, and so does one carrying actions; a strip of plain `done` rows does not. The strip cannot answer this for itself: what it holds is the consumer's, and Slint's tab walk is pre-order, so the scrolling surface's own stop would land \*in front of\* whatever the rows own. Left false the strip takes the stop and the arrows scroll it, which is right for inert rows and costs one extra Tab in front of live ones. |

## Accessibility

- **The remove control is named.** It is icon-only, so `remove-label` is what a screen reader addresses it by; it is also yours to translate, and to say *what* is being removed where a strip holds several files.
- **No affordance, no tab stop.** A row without `removable` offers nothing to click where the control would sit and nothing for Tab to land on, and a row that is not `interactive` is not announced as a control at all.
- **An interactive row is a button** named by `action-label`, activated by the pointer, by `Enter`, by `Space` and by the accessible default action — one path, so they cannot drift apart.
- **The trigger never traps the actions.** The row’s control layer sits under the slotted actions and the remove control, so each keeps its own press and its own place in the tab order.
- **A failed upload says why.** The destructive edge is the second channel; the reason belongs on `meta`, where assistive technology finds it.
- **A transfer is readable, not merely visible.** `uploading` and `processing` put a real `Progress` on the row rather than animating the title, so how far it has got is in the accessibility tree.
- **The group is a `groupbox`** named by `label`, so a composer with two trays can tell them apart.
- **The strip is a stop in the reading order** for as long as it has somewhere to scroll (ADR-0032): `End` walks it to an attachment past the edge without a pointer.
- **An attachment past the edge is still reachable.** The only thing hiding it is the scrolling surface’s own `Flickable`, so it keeps its place in the tab order — and `Tab`ing onto a control inside it scrolls the strip to it, because the reveal happens in the window that moved the focus rather than in the group (ADR-0038).
