Attachments
Composable system for displaying file attachments — inline badges in a prompt input, grid cards in a message bubble, or full-detail rows in an upload UI. Works standalone or inside PromptInput.
Preview — grid
<attachment-list variant="grid">
@for (item of attachments(); track item.id) {
<attachment-item [data]="item" (removed)="onRemove($event)">
<attachment-preview />
<attachment-info [showMediaType]="true" />
<attachment-remove />
</attachment-item>
}
</attachment-list>Preview — inline
<attachment-list variant="inline">
@for (item of attachments(); track item.id) {
<attachment-item [data]="item" (removed)="onRemove($event)">
<attachment-preview />
<attachment-name />
<attachment-remove />
</attachment-item>
}
</attachment-list>Preview — list
<attachment-list variant="list">
@for (item of attachments(); track item.id) {
<attachment-item [data]="item" (removed)="onRemove($event)">
<attachment-preview />
<div class="flex min-w-0 flex-1 flex-col">
<attachment-name />
<attachment-size />
</div>
<attachment-remove />
</attachment-item>
}
</attachment-list>Empty state
Drop files here or paste an image.
<attachment-list>
<attachment-empty>
<p class="text-sm">Drop files here or paste an image.</p>
</attachment-empty>
</attachment-list>Anatomy
text
<attachment-list variant="grid | inline | list">
├── <attachment-item [data]="..." (removed)="...">
│ ├── <attachment-preview [fallback]="..." /> Image / video / icon
│ ├── <attachment-info [showMediaType]="..." /> Name + secondary line
│ │ — or finer-grained:
│ ├── <attachment-name /> Filename only
│ ├── <attachment-size /> Formatted bytes
│ └── <attachment-remove [label]="..." /> Close button
└── <attachment-empty> Empty-state slot
Custom message via <ng-content>
</attachment-empty>Installation
bash
npm install @shadng/attachments @shadng/coreOr via the CLI:
bash
npx shadng add attachmentsBasic usage
typescript
import { Component, signal } from '@angular/core';
import {
AttachmentInfo,
AttachmentItem,
AttachmentList,
AttachmentPreview,
AttachmentRemove,
toAttachmentData,
type AttachmentData,
} from '@shadng/attachments';
@Component({
selector: 'app-uploader',
imports: [
AttachmentList,
AttachmentItem,
AttachmentPreview,
AttachmentInfo,
AttachmentRemove,
],
template: `
<attachment-list variant="grid">
@for (item of items(); track item.id) {
<attachment-item [data]="item" (removed)="onRemove($event)">
<attachment-preview />
<attachment-info [showMediaType]="true" />
<attachment-remove label="Remove this file" />
</attachment-item>
}
</attachment-list>
`,
})
export class Uploader {
items = signal<AttachmentData[]>([]);
onFileSelected(file: File) {
this.items.update((prev) => [...prev, toAttachmentData(file)]);
}
onRemove(item: AttachmentData) {
this.items.update((prev) => prev.filter((it) => it.id !== item.id));
}
}Variants
- grid (default) — wraps cards with a thumbnail header. Best for message bubbles and rich previews.
- inline — compact badge row. Best for the row above a prompt input's textarea.
- list — vertical stack with full metadata. Best for upload UIs.
Composition with PromptInput
When nested inside a PromptInput, the attachment state is owned by the container. Bind the attachments model both ways and wire the list:
typescript
<prompt-input
[(value)]="text"
[(attachments)]="attachments"
[acceptAttachments]="['image/*', 'application/pdf']"
(submitted)="send($event)"
>
@if (attachments().length > 0) {
<attachment-list variant="inline">
@for (item of attachments(); track item.id) {
<attachment-item [data]="item" (removed)="removeAttachment($event)">
<attachment-preview />
<attachment-name />
<attachment-remove />
</attachment-item>
}
</attachment-list>
}
<prompt-input-textarea />
<prompt-input-toolbar>
<prompt-input-submit />
</prompt-input-toolbar>
</prompt-input>API — AttachmentData
The shape every item accepts. Compatible with FileUIPart from the Vercel AI SDK.
| Name | Type | Description |
|---|---|---|
| id | string | Stable identifier — used for tracking in @for loops. |
| filename | string? | Display name. Falls back to a category label ("Image", "Document") when omitted. |
| mediaType | string? | MIME type. Drives the preview surface and label fallback. |
| url | string? | Already-uploaded URL — used as <img>/<video> source. |
| file | File? | Native File — used to derive a blob URL for image previews. |
| size | number? | Bytes. When omitted but `file` is set, derived from file.size. |
API — AttachmentList
| Name | Type | Default | Description |
|---|---|---|---|
| variant | 'grid' | 'inline' | 'list' | 'grid' | Layout shape. Items read this from the list via DI. |
| ariaLabel | string | 'Attachments' | aria-label applied to the role="list" host. |
API — AttachmentItem
| Name | Type | Default | Description |
|---|---|---|---|
| data | AttachmentData | (required) | The attachment to render. |
| loading | boolean | false | Show animated skeleton overlay on the preview. |
| errored | boolean | false | Destructive border + "Error" badge. |
| variant | 'grid' | 'inline' | 'list' | undefined | undefined | Override the parent list variant for a single item. |
API — AttachmentItem outputs
| Name | Type | Description |
|---|---|---|
| removed | AttachmentData | Emitted when AttachmentRemove inside the item is clicked. Carries the data object. |
API — AttachmentPreview
| Name | Type | Default | Description |
|---|---|---|---|
| fallback | TemplateRef<unknown> | null | null | Custom non-image surface (your own icon system, custom layout, etc). |
API — AttachmentInfo
| Name | Type | Default | Description |
|---|---|---|---|
| showMediaType | boolean | false | Add the MIME type to the secondary line. |
| showSize | boolean | true | Add the formatted file size to the secondary line. |
API — AttachmentRemove
| Name | Type | Default | Description |
|---|---|---|---|
| label | string | 'Remove attachment' | Screen-reader label and tooltip. |
Utility functions
Re-exported from @shadng/attachments:
| Name | Type | Description |
|---|---|---|
| toAttachmentData(file, overrides?) | (File, Partial<AttachmentData>?) => AttachmentData | Convert a native File into the shape components consume. Generates an id if you do not provide one. |
| getMediaCategory(data) | (data) => "image" | "video" | "audio" | "document" | "source" | "unknown" | Coarse classification driving the preview surface. |
| getAttachmentLabel(data) | (data) => string | Best human-readable label — filename, then category fallback, then "Attachment". |
| formatFileSize(bytes) | (number | undefined) => string | Format bytes as a short string like "240 KB" or "12.4 MB". |
Accessibility
- role="list" on the AttachmentList container, configurable
ariaLabel - role="listitem" on each AttachmentItem, with
data-media-categoryfor CSS targeting - Remove button has an explicit
aria-label(default "Remove attachment") - Preview images get an
altderived from the filename — pass a more descriptive value via your ownfallbacktemplate when needed - Errored state surfaces a visible "Error" badge in addition to the destructive border
Theming
Semantic tokens consumed:
--card/--card-foreground— item background--border— item border--muted/--muted-foreground— preview background, secondary text--accent/--accent-foreground— remove button hover--destructive/--destructive-foreground— errored item border + error badge--ring— focus rings
The component does not ship its own tokens — see Theme tokens for the full list.
Known issues
- Hover cards deferred to v0.2. AI Elements ships an AttachmentHoverCard for showing a larger preview on hover in inline variant. ShadNG will adopt it once the popover primitive lands (target: v0.2 alongside Conversation).
- id generation.
toAttachmentData()uses an internal counter for ids. If you persist attachments across reloads, generate stable ids yourself.