Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Attachment

A file chip / thumbnail card for chat and message-compose surfaces — media slot, name + meta, uploading/error/complete states, and remove/download actions.

Status
stable
Since
0.1.0
Accessibility pattern
labelled media tile

Last updated

release-notes.pdf248 KB

Install

Add Attachment from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/attachment

The same command installs the registry items it composes: @vegastack/spinner, @vegastack/progress.

Usage

import { X } from "lucide-react";
import {
  Attachment,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment";
import { IconButton } from "@/components/ui/icon-button";
import { FileText } from "lucide-react";

<Attachment state="complete">
  <AttachmentMedia>
    <FileText />
  </AttachmentMedia>
  <AttachmentContent>
    <AttachmentTitle>release-notes.pdf</AttachmentTitle>
    <AttachmentDescription>248 KB</AttachmentDescription>
  </AttachmentContent>
  <AttachmentActions>
    <IconButton aria-label="Remove release-notes.pdf" variant="ghost" size="xs">
      <X />
    </IconButton>
  </AttachmentActions>
</Attachment>;

Anatomy

There is no headless @shadcn/react Attachment primitive published (0.2.1 exports only ./message-scroller), so — like the rest of the chat family (Message, Bubble, Marker) — Attachment is fully self-owned: plain CVA + data-slot/data-* styling, no new npm dependency.

Attachment — data-slot="attachment"
AttachmentActions — data-slot="attachment-actions"
AttachmentContent — data-slot="attachment-content"
AttachmentDescription — data-slot="attachment-description"
AttachmentGroup — data-slot="attachment-group"
AttachmentMedia — data-slot="attachment-media" | "attachment-media-overlay"
AttachmentProgress — data-slot="attachment-progress"
AttachmentTitle — data-slot="attachment-title"
AttachmentTrigger
  • Attachment — the root chip/card (data-slot="attachment"). Owns state, size, orientation.
  • AttachmentMedia — the leading file-type icon or image thumbnail (data-slot="attachment-media", decorative aria-hidden). Shows an inline Spinner overlay automatically while state="uploading".
  • AttachmentContent — the vertical column of title / description / progress.
  • AttachmentTitle — the file name. Always truncates.
  • AttachmentDescription — the meta line (size, upload %, error message). Pass live to make it an aria-live="polite" region.
  • AttachmentProgress — a thin determinate bar (wraps Progress) for the uploading state.
  • AttachmentActions — the trailing action row. Compose IconButton directly — its type-level aria-label requirement covers the "remove button needs an accessible name" contract, so there is no separate AttachmentAction wrapper.
  • AttachmentTrigger — an optional invisible overlay that turns the whole card into one activation target (open/download). Built on Base UI useRender.
  • AttachmentGroup — wraps multiple attachments in a message-compose row.

Examples

File chip

The default anatomy: a file-type icon, name + size, and a remove action.

release-notes.pdf248 KB

Image thumbnail

orientation="vertical" stacks the media on top; AttachmentMedia variant="image" expects a filled thumbnail — compose Image (aspectRatio="square" className="size-full") for a real, resolved URL. AttachmentActions floats over the corner and an AttachmentTrigger makes the whole card open the file.

cover-photo.png1920×1080 · 3.4 MB

Uploading

state="uploading" shimmers the title and reveals the AttachmentMedia spinner overlay automatically. Pair AttachmentProgress (the visual bar) with a live AttachmentDescription (the text screen readers actually hear as the percentage changes).

quarterly-report.xlsxUploading — 67%
x

Error

state="error" tints the border, media, title, and description with destructive tokens. Give the AttachmentDescription live so the failure reason is announced.

budget-2027.numbersUpload failed — file type not supported

Removable compose list

AttachmentGroup wraps several size="sm" attachments under a message-compose input; each IconButton remove action drives real client state.

release-notes.pdf248 KB
quarterly-report.xlsx1.1 MB
roadmap.docx84 KB

Playground

Step through the lifecycle states, densities, orientations, and media treatments, then copy the generated JSX.

release-notes.pdf248 KB
<Attachment>
  <AttachmentMedia>
    <FileText />
  </AttachmentMedia>
  <AttachmentContent>
    <AttachmentTitle>release-notes.pdf</AttachmentTitle>
    <AttachmentDescription>248 KB</AttachmentDescription>
  </AttachmentContent>
</Attachment>

API Reference

Attachment

PropTypeDefaultDescription
orientationAttachmentOrientation'horizontal'Layout.
sizeAttachmentSize'md'Density.
stateAttachmentState'idle'Lifecycle state. - idle: attached, not yet acted on. - uploading: in flight — pair with AttachmentProgress and a live AttachmentDescription. - error: failed — tints the border/media/description with destructive tokens. - complete: finished/delivered. - disabled: visually dimmed and inert (pointer-events-none); also pass disabled to any interactive descendant (e.g. IconButton) so it is unreachable by keyboard, not just the mouse.

Data attributes and CSS variables on Attachment

AttributeValues
data-orientationmirrors a prop or state value
data-sizemirrors a prop or state value
data-slot"attachment"
data-statemirrors a prop or state value

AttachmentMedia

PropTypeDefaultDescription
variantAttachmentMediaVariant'icon'icon centers a bare lucide-react glyph; image expects a filled thumbnail (compose "@/components/ui/image" Image with aspectRatio="square" and className="size-full").

Data attributes and CSS variables on AttachmentMedia

AttributeValues
data-slot"attachment-media" | "attachment-media-overlay"
data-variantmirrors a prop or state value

AttachmentDescription

PropTypeDefaultDescription
livebooleanfalseMark this line as a polite live region — announce its text as it changes. Set this whenever the description shows transient status copy (an upload percentage, an error message) so screen reader users hear the update without having to re-focus the attachment. Leave false for a static meta line (e.g. a plain file size) that never changes on its own.

Data attributes and CSS variables on AttachmentDescription

AttributeValues
data-slot"attachment-description"

AttachmentProgress

PropTypeDefaultDescription
aria-label*stringAccessible name for the underlying progress bar — required (there is no visible label). Include the file name so assistive tech can tell multiple in-flight attachments apart, e.g. "quarterly-report.xlsx upload progress".
indicatorClassNamestringClasses for the fill indicator. Use this to change the fill token or motion treatment without replacing the accessible progress root.
maxnumber100Upper bound of the scale — value is reported as value / max.
renderComponentRenderFn<HTMLProps, ProgressRootState> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>>Replace the rendered root element via Base UI render composition. Pass a ReactElement or a render function — Base UI merges this wrapper's data-slot and state data-* onto your element, forwards the ref, and keeps the <Progress.Track> / <Progress.Indicator> children. The element must support role="progressbar" semantics.
trackClassNamestringClasses for the inner track rail. Use this for track width/height/color overrides; className belongs to the root progressbar element.
valuenumbernullCurrent completion value, between 0 and max. Pass null for an indeterminate bar (Base UI sets data-indeterminate and drops aria-valuenow).

Data attributes and CSS variables on AttachmentProgress

AttributeValues
data-slot"attachment-progress"

AttachmentTrigger

PropTypeDefaultDescription
renderuseRender.RenderProp<Record<string, unknown>>Render the trigger as a different element (e.g. an a to open/download the file) via Base UI render composition. Pass a ReactElement or a render function.

AttachmentGroup / AttachmentContent / AttachmentTitle / AttachmentActions

Plain layout wrappers with no own props — they accept standard <div> / <span> props (className, children, …).

Accessibility

  • AttachmentMedia is decorative (aria-hidden) — AttachmentTitle (the file name) carries the accessible identity, never the icon or thumbnail alone.
  • The remove/download action is a real IconButton, which requires aria-label at the type level — always include the file name in it (e.g. "Remove release-notes.pdf") so multiple attachments stay distinguishable to screen reader users.
  • Transient state changes (an upload percentage, an error message) are not announced by color or a bare role="progressbar" alone — pass live to AttachmentDescription so the text is read out via role="status" aria-live="polite", the same pattern as AutoSaveInput and CopyButton.
  • state="disabled" visually dims the card and sets pointer-events-none + aria-disabled, but it cannot reach into children — also pass disabled to any nested IconButton so it is unreachable by keyboard, not just the mouse.
  • AttachmentTrigger is a real button covering the card, and it shows the system's standard :focus-visible outline, inset so the card's rounded corners do not clip it. It does not rely on a border tint alone — a tint is the text-entry treatment, not a button's.
  • AttachmentTrigger and AttachmentActions share the same z-(--z-raised) stacking band; place AttachmentTrigger first in JSX so the actions stay on top and clickable.
ContractStates tested
Behaviourdefault, complete, disabled, error, open, uploading
Accessibilitydisabled, focus-visible, labeled, live, status-announcement, semantic-html
Visualdefault, focus, error

Do / Don't

Do
Pair a live AttachmentDescription with AttachmentProgress while uploading, and always include the file name in the remove IconButton's aria-label.
Don't
Rely on the border/icon color alone to convey the error state, or drop AttachmentTitle's truncation by overriding className with `whitespace-normal`.

On this page