Skip to content
Component installs need the registry setup
VegaStack Design

Upload Panel

The uploads card pinned bottom-end — files and folders in flight, progress, failures and destinations — with a done card and a phone sheet.

Status
stable
Since
0.23.120
Accessibility pattern
named region, labelled progress rings, milestone announcer

Last updated

Uploading 3 itemsAbout 2 minutes left · 11 MB of 256 MB

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Upload Panel before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify upload-panel

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/upload-panel

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/alert-dialog, @vegastack/button, @vegastack/file-kind, @vegastack/image, @vegastack/progress, @vegastack/responsive-dialog, @vegastack/upload-progress, @vegastack/use-announcer, @vegastack/use-mobile.

Usage

import { UploadPanel } from "@/components/ui/upload-panel";

// Mount once in the app shell, so uploads survive navigation.
<UploadPanel
  items={uploads.items}
  summary={uploads.summary}
  collapsed={collapsed}
  onCollapsedChange={setCollapsed}
  onCancel={uploads.cancel}
  onRetry={uploads.retry}
  onResume={uploads.chooseFileAgain}
  onOpen={(href) => router.push(href)}
  onCancelAll={uploads.cancelAll}
  onRetryFailed={uploads.retryFailed}
  onDismiss={uploads.clear}
/>;

The panel is presentational and controlled: the host's upload engine owns the queue, the bytes and the retries, and passes items (files, and folders with their files) and a summary (counts, bytes, timeLeftMs and the batch status). The time left comes from createRateEstimator in the upload-progress lib, which smooths acknowledged bytes and holds the estimate until it moves by more than 15%; formatTimeLeft turns it into "About 2 minutes left".

import {
  createRateEstimator,
  formatTimeLeft,
  formatBytesProgress,
} from "@/lib/upload-progress";

const estimator = createRateEstimator();
estimator.sample(bytesAcked, performance.now());
const timeLeftMs = estimator.timeLeftMs(
  total - bytesAcked,
  bytesAcked / total,
  performance.now(),
);
Uploading 3 itemsAbout 2 minutes left · 11 MB of 256 MB

Scope

BehaviourWhere it lives
Rows, states, the done card, confirm, phone sheetUploadPanel, UploadItem, UploadGroup
Speed, time left and bytes wordingupload-progress lib
Kind, icon, tint and label of each filefile-kind lib (FileTypeIcon)
Choosing filesUploadDialog, Dropzone
Uploading, resuming, retryingHost — the engine calls back into its queue
Toasts clearing the panelToast reads --upload-panel-inset, which the panel publishes itself

Anatomy

UploadPanel — data-slot="upload-panel" | "upload-panel-footer" | "upload-panel-header" | "upload-panel-list" | "upload-panel-progress"
UploadItem — data-slot="upload-done-icon" | "upload-item" | "upload-item-state"
UploadGroup — data-slot="upload-group" | "upload-group-files" | "upload-item-state"

Examples

Collapsed

The chevron folds the list to the header, with one bar for the whole batch.

Uploading 3 itemsAbout 2 minutes left · 11 MB of 256 MB
x

Folder

A folder upload is one row with its count and aggregate progress; its chevron expands the files. Cancel and Retry on the folder call back with the folder's id.

Uploading 7 itemsLess than a minute left · 14 MB of 31 MB

Errors

A failure keeps the panel open until the person acts: the reason under the name, Retry on the row and Retry failed below the list. An interrupted upload (the page reloaded mid-upload) asks for its file again with Choose file, and a cancelled one is muted.

1 of 4 uploads failed

Done

When everything finishes cleanly the panel shrinks into a done card that calls onDismiss after autoDismissMs (8 s), paused while it is hovered or focused. Its chevron reopens the list.

4 uploads complete

Phone

Below 768px the panel is a compact bar with the batch's progress; tapping it opens every row in a bottom sheet. compact asks for the bar at any width.

x

File types

FileTypeIcon resolves every row's icon and tint from one table: MIME type first, the extension when the type is missing or generic.

Imagephoto.heic
PDFspec.pdf
Documentbrief.docx
Textnotes.md
Spreadsheetbudget.xlsx
Dataexport.csv
Presentationpitch.pptx
Videotour.mov
Audiomemo.m4a
Archivehandover.zip
Codeindex.ts
JSONconfig.json
Configdeploy.yaml
Scriptsetup.sh
CADplan.dwg
PhotometricLX200.ies
Designrender.psd
eBookmanual.epub
Emailthread.eml
Calendarvisit.ics
Contactpriya.vcf
Keyserver.pem
Encryptedsecrets.gpg
FontGeist.woff2
Filedata.bin

API Reference

PropTypeDefaultDescription
items*UploadEntry[]—The rows: files and folders, in the order to show them. An empty list renders nothing.
summary*UploadSummary—The batch totals and state.
autoDismissMsnumber8000How long the done card stays before it calls onDismiss, paused while hovered or focused; 0 keeps it until closed.
collapsedbooleanfalseWhether the list is folded to the header (controlled; ignored by the compact bar, which opens the list in a sheet).
compactboolean—Show the compact bar that opens the list in a sheet (a dialog on a wide screen). Unset, it follows the viewport: the bar below 768px, the card from there up.
labelsPartial<UploadPanelLabels>—Any wording to replace.
maxRowsnumber200The most rows it renders; the rest are counted on a closing line.
onCancel((id: string) => void)—Cancel a queued or uploading file, or a folder's remaining files, by id. Without it no cancel button shows.
onCancelAll(() => void)—Cancel everything still moving (the close confirm and the footer's Cancel all).
onCollapsedChange((collapsed: boolean) => void)—Called when the person folds or unfolds the list.
onDismiss(() => void)—The panel asks to go: closed when idle, after a confirmed cancel, or by the done card's timer.
onOpen((href: string) => void)—Open a destination in-app; without it a destination link navigates by its href.
onResume((id: string) => void)—Resume an interrupted file by id: the host asks for the file again ("Choose file").
onRetry((id: string) => void)—Retry a failed file, or a folder's failed files, by id. Without it no Retry button shows.
onRetryFailed(() => void)—Retry every failed file (the footer's Retry failed).

Data attributes and CSS variables on UploadPanel

AttributeValues
data-slot"upload-panel" | "upload-panel-footer" | "upload-panel-header" | "upload-panel-list" | "upload-panel-progress"
data-state"collapsed" | "done" | "expanded"
data-statusmirrors a prop or state value

UploadItem

PropTypeDefaultDescription
file*UploadFile—The file the row shows.
onCancel((id: string) => void)—Cancel a queued or uploading file, or a folder's remaining files, by id. Without it no cancel button shows.
onOpen((href: string) => void)—Open a destination in-app; without it a destination link navigates by its href.
onResume((id: string) => void)—Resume an interrupted file by id: the host asks for the file again ("Choose file").
onRetry((id: string) => void)—Retry a failed file, or a folder's failed files, by id. Without it no Retry button shows.

Data attributes and CSS variables on UploadItem

AttributeValues
data-slot"upload-done-icon" | "upload-item" | "upload-item-state"
data-statusmirrors a prop or state value

UploadGroup

PropTypeDefaultDescription
folder*UploadFolder—The folder the row shows.
defaultExpandedbooleanfalseWhether its files show (uncontrolled).
maxRowsnumber200The most file rows it renders when expanded.
onCancel((id: string) => void)—Cancel a queued or uploading file, or a folder's remaining files, by id. Without it no cancel button shows.
onOpen((href: string) => void)—Open a destination in-app; without it a destination link navigates by its href.
onResume((id: string) => void)—Resume an interrupted file by id: the host asks for the file again ("Choose file").
onRetry((id: string) => void)—Retry a failed file, or a folder's failed files, by id. Without it no Retry button shows.

Data attributes and CSS variables on UploadGroup

AttributeValues
data-expandedmirrors a prop or state value
data-slot"upload-group" | "upload-group-files" | "upload-item-state"
data-statusmirrors a prop or state value

Accessibility

  • The panel is a region named "Uploads"; the list is a list, and each folder's files a nested list.
  • Each uploading row has a progressbar named by its file, with its percentage.
  • A polite live region announces milestones only — "Uploading 3 items", "3 uploads complete", "1 upload failed" — never each percent.
  • Cancel, Retry and Choose file are buttons named with the file. The cancel button takes the ring's place on hover or focus, and is always shown on a touch screen.
  • Closing while uploads are moving opens an alert dialog with focus on "Keep uploading".
  • The success check and the card's arrival collapse to their end state under reduced motion.
KeyAction
TabMove through the header, rows and footer.
Enter / SpaceCollapse, close, cancel, retry or expand.
EscIn the confirm or the sheet: close it.
ContractStates tested
Behaviouruploading, collapsed, done-card, failed, interrupted, folder, confirm-close, phone-sheet
Accessibilitylabeled, semantic-html, live-region, progressbar, browser-accessibility-test
Visualuploading, collapsed, done, failed, folder, phone

Do / Don't

Do
Mount one panel in the app shell, fed by one upload engine, so uploads keep going across pages.
Don't
Mount a panel per page, or report each upload with a toast.

On this page