File Display

Free

File attachment item with a MIME-type-aware icon badge, formatted size, upload progress, success/error status and download/remove actions — in card, row or compact layouts.

</>sv-file-display fileattachmentupload

Complexity tier: Interactive The primitive bar, plus declared async states, a keyboard model, focus restoration and undo for destructive actions.

import { SvFileDisplayComponent } from '@styloviz/commerce';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Uploading with progress

example.htmlAngular
<sv-file-display
  fileName="video.mp4"
  mimeType="video/mp4"
  status="uploading"
  [progress]="62" />

Compact row, image preview

example.htmlAngular
<sv-file-display
  fileName="cover.png"
  mimeType="image/png"
  previewUrl="/thumb/cover.png"
  variant="compact" />

A delete that failed

example.htmlAngular
<sv-file-display
  [fileName]="file.name"
  [mimeType]="file.type"
  [removing]="deleting().has(file.name)"
  [removeError]="deleteError()"
  (remove)="onRemove($event)" />

Deleting a file

example.htmlAngular
// There is no rollback() here, unlike every other write in the kit: this
// component renders ONE file, and the list it sits in belongs to you, so it has
// nothing to put itself back into. What it offers instead is the step before —
// an inline confirmation, on by default.
onRemove(event: FileRemoveEvent): void {
  this.deleteError.set(null);
  this.deleting.update(s => new Set(s).add(event.fileName));

  // requestKey makes the delete idempotent: a retried request cannot delete
  // whatever has since taken the same name.
  this.api.deleteFile(event.fileName, event.requestKey).subscribe({
    next: () => this.files.update(list => list.filter(f => f.name !== event.fileName)),
    error: () => this.deleteError.set('The file could not be deleted.'),
    complete: () => this.deleting.update(s => { const n = new Set(s); n.delete(event.fileName); return n; }),
  });
}

The download is not a safety claim

scan defaults to 'unscanned', which is not a synonym for 'clean'. An unlabelled one-click download on every file asserts, on your behalf, that the file is safe to open — the one claim a file viewer must never make by omission. 'pending' withholds the download until the check finishes; 'flagged' withholds it outright and says why, unless allowUnsafeDownload puts the bytes behind an explicit confirmation for the people who genuinely need them.

example.htmlAngular
<sv-file-display
  [fileName]="file.name"
  [mimeType]="file.type"
  [scan]="file.scan"
  [scanNote]="file.scanNote"
  [allowUnsafeDownload]="user.canTriageQuarantine"
  (download)="fetch(file)" />

Retrying — or resuming — a failed upload

A failed upload used to say "Upload failed" beside the size and offer nothing: no reason, and no way to try again, so the only route back was to remove the row and pick the file a second time. The retry control states which of the two acts it performs, because a "Retry" that silently re-sends 900 MB over a metered connection is a different offer from resuming.

example.htmlAngular
<sv-file-display
  [fileName]="file.name"
  [fileSize]="file.size"
  [status]="file.status"
  [uploadError]="file.error"
  [resumable]="true"
  [uploadedBytes]="file.sent"
  (retryUpload)="resume($event)"
  (cancelUpload)="abort($event.fileName)" />

// resumeFrom is 0 unless you said BOTH that the upload is resumable and how
// far it got — either half missing means the bytes have to go again.
resume(e: FileRetryEvent) {
  this.api.upload(e.fileName, { offset: e.resumeFrom, key: e.requestKey });
}

When to use

  • Listing uploaded or attached files with type, size and per-item actions.
  • You want a built-in upload progress bar and success/error states.
  • For the upload dropzone itself, use the File Upload component.

Async states

None — this component has nothing to load, so it has no loading, empty or error state to drive.

Server writes

Writes
  • remove
  • retryUpload

Each payload carries a requestKey — send it with the request so a retry cannot apply the same change twice.

Rollback
None — this component renders a single item out of a collection you own, so there is nothing here to put back. It asks for confirmation first instead, and the item only moves when you change the data.
While saving
Bind removing to stop the action being repeated while the request is out.
On failure
Pass the failure to uploadError to show it in place — without taking away what the user was working on.

Inputs

NameTypeDefaultDescription
fileNamestring''File name including extension.
fileSizenumber | undefinedundefinedFile size in bytes (auto-formatted).
mimeTypestring''MIME type string for icon selection.
statusFileStatus'idle'Current file status.
progressnumber | undefinedundefinedUpload progress 0–100. Only shown when status = 'uploading'.
previewUrlstring''Image preview URL (shown for image MIME types).
removablebooleantrueShow remove button.
downloadablebooleantrueShow download button.
variantFileVariant'card'Layout variant.
scanFileScanState'unscanned'What the host knows about this file's safety.
scanNotestring''The host's own wording for a `'flagged'` file — what the scanner matched, or which policy it broke. Shown in place of the generic sentence, because "Contains a macro that writes to disk" is actionable and "Flagged as unsafe" is not.
allowUnsafeDownloadbooleanfalseWhether a `'flagged'` file may be downloaded at all, behind a confirmation. Off by default, and the override is a HOST decision rather than a user one: an analyst triaging a quarantine queue needs the bytes, and an ordinary user of a file list does not. When it is off there is no button to press, no disabled control to hover, and no ambiguity about whether pressing harder would work — just the reason the file is blocked.
uploadErrorSvDataError | string | nullnullWhy the upload failed. Shown beside the retry control when `status` is `'error'`.
resumablebooleanfalseWhether the host can resume this upload rather than starting over.
uploadedBytesnumber | undefinedundefinedBytes already accepted by the server. Only meaningful with `resumable`, and it is what turns *Retry* into *Resume from 412 MB*.
confirmRemovebooleantrueAsk before deleting. Defaults to ON, and the previous behaviour was a single click that emitted straight through. Deleting a file is not undoable by anything the component can offer — see FileRemoveEvent — and the button is a 28px × next to a download button of the same size, so the guard is the only thing standing between a misplaced click and a lost upload. Turn it off only where the host runs its own confirmation dialog.
removingbooleanfalseTrue while the delete is being sent. Both the confirm and cancel buttons stop responding, so the request cannot be fired twice.
removeErrorSvDataError | string | nullnullThe delete failed. Reported in place, because the row is still here — the host never took it out.

Outputs

EventPayload typeDescription
removeFileRemoveEventThe user confirmed deleting the file. Bind this or nothing happens: the component only asks, it never deletes.
downloadvoidEmitted when the download button is clicked.
retryUploadFileRetryEventRetry (or resume) a failed upload. Carries `resumeFrom`, which is `0` unless the host declared the upload resumable and said how far it got.
cancelUpload{ fileName: string }Abandon an upload that is still running.

Customization & theming

  • Three layouts via `variant`: card, row, compact.
  • Toggle the `removable` and `downloadable` actions independently.
  • Drive the progress bar and status chip with `status` + `progress`.
  • `confirmRemove` (on by default) asks inline before emitting `(remove)`. Set it to `false` when the host runs its own confirmation dialog, and the × emits straight through.
  • `removing` locks the × and the confirm button while the delete is in flight; `removeError` reports a failed delete under the row, which is still there because the host never took it out.
  • `(remove)` carries `{ fileName, requestKey }`. There is deliberately no `rollback()`: one file cannot put itself back into a list it does not own.
  • `scan` (`unscanned` / `pending` / `clean` / `flagged`) drives both the disclosure and whether a download is offered at all; `scanNote` supplies your own wording and `allowUnsafeDownload` puts a flagged file behind a confirmation instead of withholding it.
  • `status="error"` renders `uploadError` and a retry control. `resumable` + `uploadedBytes` turn it into "Resume from 412 MB" and put the offset in `(retryUpload)`; `(cancelUpload)` abandons one that is still running.

Best practices

  • Do: Pass a real `mimeType` so the icon colour and image preview resolve correctly.
  • Do: Wrap multiple items in a `role="list"` container so each file reads as a list entry.
  • Tip: Hide actions you do not support with `removable`/`downloadable` rather than disabling them.
  • Do: Leave `confirmRemove` on unless you run your own dialog. The × is a 28px target next to a same-size download button, and the delete is the one write here that nothing can undo.
  • Do: Bind `removing` while the delete is in flight so it cannot be sent twice, and send `requestKey` so a retry cannot delete whatever has since taken the same name.
  • Don't: Don't remove the row yourself before the server confirms. There is no `rollback()` here — the list is yours, so the component has nothing to put itself back into — and `removeError` reports the failure in place, on a row that is still there.
  • Do: Set `scan` from what you actually know. Leaving it at `'unscanned'` is honest; setting it to `'clean'` because it looks tidier tells the user a scanner cleared a file no scanner ever saw.
  • Do: Give a flagged file a `scanNote`. "Matched EICAR-Test-File" is actionable; "Flagged as unsafe" is not.
  • Do: Bind `uploadError` alongside `status="error"`. The retry button appears either way, but a retry with no stated reason invites the same failure a second time.
  • Don't: Don't set `resumable` unless the server really accepts a byte offset. The label promises the user their partial upload survives, and `resumeFrom` is the number that has to honour it.
  • Don't: Don't leave `allowUnsafeDownload` on for ordinary users. It exists for an analyst triaging a quarantine queue, not for a file list.

Accessibility

  • Each item is a labelled `group` named by its file; download/remove buttons include the file name so they are unambiguous in a list.
  • The × keeps its place while the confirmation is open rather than being swapped out, so the focus that pressed it still has somewhere to be. It exposes `aria-expanded`, and its label becomes "Cancel removing <file>".
  • The confirmation is a labelled `group` whose text says the delete cannot be undone — not colour alone. A failed delete renders as `role="alert"`.
  • The progress bar is a labelled `progressbar` exposing its current value; success/error indicators are exposed as labelled images.
  • Image previews require `alt` (derived from the file name).
  • The scan state is written in words, not conveyed by a colour or a glyph alone — "Not checked for threats" and "Checked — no threats found" must not look like each other. In the compact layout, where there is no room for the sentence, the glyph carries it as its accessible name.
  • A withheld download is absent rather than disabled, and the reason is stated beside it: a greyed control answers nothing and invites a harder press.
  • The download button keeps its place while the unsafe-download confirmation is open, exposes `aria-expanded`, and the strip that follows it is the next tab stop.

Feedback on File Display

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.