Attachments
Render files and source documents attached to a chat turn — grid thumbnails, inline chips, or a detailed list, with hover previews and removal.
Overview
Attachments renders the files and source documents bound to a chat turn — images, PDFs, audio, video, or cited sources. It is a compound, context-driven primitive: the Attachments container sets the variant (grid, inline, or list), and every Attachment child reads that variant to lay itself out, pick its media icon, and size its preview. Drop in only the sub-parts you need (AttachmentPreview, AttachmentInfo, AttachmentRemove).
Each Attachment takes an AttachmentData — a FileUIPart or SourceDocumentUIPart from the ai package, plus an id. The media category (image / video / audio / document / source) is inferred from mediaType, so the right icon and preview render without extra config. Pass onRemove to surface the remove button; omit it and the button renders nothing.
Preview
'use client';import { Attachment, AttachmentInfo, AttachmentPreview, AttachmentRemove, Attachments, type AttachmentData,} from '@gremorie/rx-ai';const files: AttachmentData[] = [ { id: '1', type: 'file', mediaType: 'image/png', filename: 'dashboard-mock.png', url: '', }, { id: '2', type: 'file', mediaType: 'application/pdf', filename: 'spec-v3.pdf', url: '', }, { id: '3', type: 'file', mediaType: 'audio/mpeg', filename: 'voice-note.mp3', url: '', },];export function AttachmentsPreview() { return ( <div className="flex flex-col gap-8"> <div className="flex flex-col gap-2"> <span className="text-muted-foreground text-xs">grid</span> <Attachments variant="grid" className="ml-0"> {files.map((data) => ( <Attachment key={data.id} data={data} onRemove={() => undefined}> <AttachmentPreview /> <AttachmentRemove /> </Attachment> ))} </Attachments> </div> <div className="flex flex-col gap-2"> <span className="text-muted-foreground text-xs">inline</span> <Attachments variant="inline"> {files.map((data) => ( <Attachment key={data.id} data={data} onRemove={() => undefined}> <AttachmentPreview /> <AttachmentInfo /> <AttachmentRemove /> </Attachment> ))} </Attachments> </div> <div className="flex flex-col gap-2"> <span className="text-muted-foreground text-xs">list</span> <Attachments variant="list"> {files.map((data) => ( <Attachment key={data.id} data={data} onRemove={() => undefined}> <AttachmentPreview /> <AttachmentInfo showMediaType /> <AttachmentRemove /> </Attachment> ))} </Attachments> </div> </div> );}Anatomy
Attachments container; sets variant (grid · inline · list) via context
└─ Attachment one file/source; reads variant, infers media category
├─ AttachmentPreview thumbnail (image/video) or media-category icon
├─ AttachmentInfo filename + optional media type (hidden in grid)
├─ AttachmentRemove remove button (renders only when onRemove is set)
├─ AttachmentHoverCard optional hover preview wrapper
│ ├─ AttachmentHoverCardTrigger
│ └─ AttachmentHoverCardContent
└─ AttachmentEmpty zero-state messageInstallation
bash npx gremorie@latest add rx-attachments bash pnpm dlx gremorie@latest add rx-attachments
bash yarn dlx gremorie@latest add rx-attachments
bash bunx --bun gremorie@latest add rx-attachments
Brings in rx-forms (for Button) and rx-overlays (for HoverCard) as registry dependencies, plus the ai package for the attachment types.
Usage
import {
Attachment,
AttachmentInfo,
AttachmentPreview,
AttachmentRemove,
Attachments,
type AttachmentData,
} from "@gremorie/rx-ai";
export function Example({
files,
remove,
}: {
files: AttachmentData[];
remove: (id: string) => void;
}) {
return (
<Attachments variant="inline">
{files.map((data) => (
<Attachment key={data.id} data={data} onRemove={() => remove(data.id)}>
<AttachmentPreview />
<AttachmentInfo />
<AttachmentRemove />
</Attachment>
))}
</Attachments>
);
}npx gremorie@latest add ng-attachmentsimport { Component, signal } from '@angular/core';
import {
AttachmentList,
AttachmentItem,
AttachmentPreview,
AttachmentInfo,
AttachmentRemove,
type AttachmentData,
} from '@gremorie/ng-ai';
@Component({
selector: 'app-example',
standalone: true,
imports: [
AttachmentList,
AttachmentItem,
AttachmentPreview,
AttachmentInfo,
AttachmentRemove,
],
template: `
<attachment-list variant="grid">
@for (file of files(); track file.id) {
<attachment-item [data]="file" (removed)="remove($event)">
<attachment-preview />
<attachment-info />
<attachment-remove />
</attachment-item>
}
</attachment-list>
`,
})
export class ExampleComponent {
readonly files = signal<AttachmentData[]>([]);
remove(file: AttachmentData) {
this.files.update((current) => current.filter((f) => f.id !== file.id));
}
}API
<Attachments>
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "grid" | "inline" | "list" | "grid" | Layout mode shared with every child via context. grid floats to the end (ml-auto). |
className | string | - | Extra classes on the container. |
Extends all HTMLAttributes<HTMLDivElement>. Provides the attachments context.
<Attachment>
| Prop | Type | Default | Description |
|---|---|---|---|
data | AttachmentData | - | A FileUIPart or SourceDocumentUIPart with an id. |
onRemove | () => void | - | When set, AttachmentRemove renders a working remove button. |
className | string | - | Extra classes on the item. |
Provides the per-item context (data, inferred mediaCategory, onRemove, variant).
<AttachmentPreview>
| Prop | Type | Default | Description |
|---|---|---|---|
fallbackIcon | ReactNode | - | Override the media-category icon shown when there is no thumbnail. |
className | string | - | Extra classes on the preview box. |
Renders an <img> for images, a <video> for videos, otherwise the media-category icon.
<AttachmentInfo>
| Prop | Type | Default | Description |
|---|---|---|---|
showMediaType | boolean | false | Render the raw mediaType under the filename. |
className | string | - | Extra classes. |
Returns null in the grid variant (thumbnails only).
<AttachmentRemove>
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "Remove" | Accessible label (aria-label + sr-only). |
Extends all Button props. Renders null unless the parent Attachment has onRemove. Stops click propagation so removing does not trigger the item.
<AttachmentHoverCard> / <AttachmentHoverCardTrigger> / <AttachmentHoverCardContent>
Thin wrappers over HoverCard from @gremorie/rx-overlays, pre-tuned for attachment previews (zero open/close delay, align="start", compact padding). Use to show a large preview on hover.
<AttachmentEmpty>
Centered muted placeholder. Defaults to "No attachments"; pass children to override.
Composition
<Attachments>picks the layout viavariantand shares it through context.<Attachment>wraps one file/source and infers its media category frommediaType.<AttachmentPreview>shows the thumbnail or media icon.<AttachmentInfo>adds the filename (and optional media type) forinline/list.<AttachmentRemove>appears only whenonRemoveis wired.
Variations
Grid (thumbnails)
The default. Square thumbnails that float to the trailing edge — ideal above a PromptInput.
<Attachments variant="grid">
{files.map((data) => (
<Attachment key={data.id} data={data} onRemove={() => remove(data.id)}>
<AttachmentPreview />
<AttachmentRemove />
</Attachment>
))}
</Attachments>Inline (chips)
Compact chips with a small icon and filename — good inside a message bubble.
<Attachments variant="inline">
{files.map((data) => (
<Attachment key={data.id} data={data}>
<AttachmentPreview />
<AttachmentInfo />
</Attachment>
))}
</Attachments>List (detailed rows)
Full-width rows with a larger preview, filename, and media type — good for a review surface.
<Attachments variant="list">
{files.map((data) => (
<Attachment key={data.id} data={data} onRemove={() => remove(data.id)}>
<AttachmentPreview />
<AttachmentInfo showMediaType />
<AttachmentRemove />
</Attachment>
))}
</Attachments>Accessibility
- Remove button:
AttachmentRemovecarries anaria-label(default"Remove") plus ansr-onlylabel, so the icon-only control is announced. - Images: previews set
altto the filename (falling back to"Image"), so screen readers describe the thumbnail. - Click isolation: removing stops propagation, so an interactive attachment row is not also triggered when the user removes it.
- Empty state:
AttachmentEmptyrenders real text, so an empty attachments region is not a silent gap.
Related
- PromptInput - the input dock attachments are staged above
- Message - attachments often render inside a message turn
- Sources - cited source documents, a sibling concept