Skip to content
Glint UI

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.

Usage

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, 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 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.

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.

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.

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.

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.

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.

API Reference

Properties

PropertyTypeDefaultDescription
namein stringno defaultFile name, shown on the first line; elides when the row is too narrow.
metain stringno defaultSecondary 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.
iconin LucideIconIconSet.PaperclipLeading icon, normally selected from IconSet in lucide-slint. Ignored while thumbnail holds an image.
thumbnailin imageno defaultPreview image; when set it replaces the icon in the media square, and fills the preview of a vertical card.
removablein boolfalseShow the trailing remove action.
remove-labelin 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.
statein AttachmentStateAttachmentState.doneWhere the upload stands.
progressin float0.0How far a transfer has got, 0..1. Read only while state is uploading; processing has no number to show and spins instead.
sizein AttachmentSizeAttachmentSize.defaultDensity.
orientationin AttachmentOrientationAttachmentOrientation.horizontalRow or card.
preview-heightin length120pxHow tall a card's preview is. Ignored by the horizontal row, whose media is the media-size square.
interactivein boolfalseMakes the whole row activatable — opening or previewing the file. The actions keep their own presses; the row's layer sits behind them.
action-labelin stringroot.nameWhat assistive technology announces the row as while it is a control.
media-sizeout lengthno defaultThe leading square's side, published because a call site laying several rows out against a gutter needs the same number.
workingout boolno defaultWhether a transfer is under way — what puts a Progress on the row.
edge-colorout colorno defaultThe row's edge, for slotted actions that want to match it.
surface-colorout colorno defaultThe surface behind the row. An idle attachment is the emptier slot.
meta-colorout colorno defaultThe meta line's colour — destructive while the reason it carries is a failure.

Callbacks

CallbackDescription
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

EnumValues
AttachmentStateidle, uploading, processing, error, done
AttachmentSizexs, sm, default
AttachmentOrientationhorizontal, vertical

AttachmentGroup

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

Properties

PropertyTypeDefaultDescription
labelin 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.
spacingin lengthTokens.spacing-smGap between two attachments.
content-takes-focusin boolfalseWhether 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 Tabing 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).