Dots

File Upload Item

One file in an upload form, from waiting and uploading to checked, accepted or rejected.

bank-statement-october.pdf
0%, 1 KB of 2.3 MB
"use client"

import * as React from "react"

FileUploadItem shows one file in an upload form: waiting, uploading with progress, being checked, accepted, or rejected with a reason and a way to replace it. Use it for documents people send for review, such as statements, payslips and ID photos. Pick it over Attachment when each file goes through a check and may come back rejected. Add files with File Trigger.

Installation

pnpm dlx shadcn@latest add @dots/file-upload-item

Usage

import { FileUploadItem } from "@/components/ui/file-upload-item"
<FileUploadItem
  name="bank-statement-october.pdf"
  state="uploading"
  progress={44}
  description="2.1 of 4.8 MB"
  onCancel={() => cancel()}
/>

Features

  • Five states: queued, uploading, checking, accepted and rejected
  • Progress bar and percentage while uploading
  • Cancel while queued or uploading, remove once settled
  • A rejection reason under the row and a replace button that opens the file picker
  • A rejected row also takes a dropped or pasted replacement
  • The file name truncates before the extension, so .pdf stays visible
  • State changes are announced politely; progress is not
  • isDisabled locks every action while the parent form submits
  • No surface of its own: stack rows in a card, sheet or form

States

Set state to follow the upload. queued is a file waiting its turn, with no progress yet. uploading shows a bar when you pass progress from 0 to 100. checking shows a spinner. accepted shows a check mark before the status.

payslip-september.pdfWaiting, 1.6 MB
bank-statement-october.pdf
44%, 2.1 of 4.8 MB
utility-bill.pdfChecking, Usually within a minute
passport-front.jpgAccepted, 1.2 MB
"use client"

import { FileUploadItem } from "@/components/ui/file-upload-item"

The buttons appear only when you pass their handlers: onCancel for queued and uploading, onRemove for accepted and rejected.

Rejected

Pass a reason that says why the file was rejected and what to send instead. With onReplace, a rejected row shows a replace button and also takes a dropped or pasted file. Replacements are checked against acceptedFileTypes; the wrong type goes to onReplaceInvalid. Check the size and content yourself in onReplace.

lease-agreement-scan.pngRejected, 3.4 MB
Your name is not visible. Send a page that shows your full name and address.
"use client"

import * as React from "react"

Icon

The tile shows a document icon by default. Pass icon to show the file type. While checking the tile shows a spinner, and once rejected a warning icon.

receipt-coffee-point.jpgAccepted, 640 KB
invoice-march.pdfAccepted, 210 KB
"use client"

import { FileUploadItem } from "@/components/ui/file-upload-item"

Disabled

Set isDisabled while the parent form submits. Cancel, remove, replace and dropping a replacement are disabled, and the row stays as it is.

passport-front.jpgAccepted, 1.2 MB
selfie-dark.jpgRejected, 0.8 MB
Your face is in shadow. Retake it facing a window.
"use client"

import * as React from "react"

Labels

Every state and button text can be changed through labels, for example to translate them. Button names get the file name appended, such as "Remove passport-front.jpg".

<FileUploadItem
  name="passport-front.jpg"
  state="checking"
  labels={{ checking: "In review" }}
/>

Accessibility

The action buttons are kit Buttons, replace is a FileTriggerButton, and with onReplace the rejected row is a React Aria DropZone that takes a pasted file from the keyboard. Without onReplace the row is a plain div.

  • Each button's accessible name includes the file name, so several rows stay distinct.
  • State changes and a new rejection reason are announced through a polite live region. Progress is not announced; the Progress bar carries its own value.
  • The reason describes the replace button. A string reason is also announced with the rejected state.
  • When the replace button disappears because the new file starts uploading, focus moves to the row's next button instead of being lost.
  • Keep the reason in words. The warning icon is decorative.

API Reference

FileUploadItem

Takes div props except children and onDrop.

PropTypeDefaultDescription
namestring-File name. Required. The extension never truncates.
state"queued" | "uploading" | "checking" | "accepted" | "rejected"-The upload state. Required.
progressnumber-0 to 100 while uploading. Without it no bar is shown.
descriptionReact.ReactNode-Meta after the state, such as "2.1 of 4.8 MB" or "1.2 MB".
reasonReact.ReactNode-Why the file was rejected and what to send instead.
iconReact.ReactNodeDocument iconIcon of the file type in the tile.
onCancel() => void-Shows the cancel button while queued or uploading.
onRemove() => void-Shows the remove button once accepted or rejected.
onReplace(file: File) => void-Shows replace on a rejected file and lets the row take a dropped file.
onReplaceInvalid(file: File) => void-Called instead of onReplace with a replacement of the wrong type.
acceptedFileTypesReadonlyArray<string>-Accepted replacements: MIME types, wildcards or extensions.
defaultCamera"user" | "environment"-Opens the camera for the replacement on phones.
isDisabledbooleanfalseDisables cancel, remove, replace and dropping a replacement.
labelsPartial<FileUploadItemLabels>-Copy for states and buttons.
classNamestring-Additional classes to apply to the row.

FileUploadState

type FileUploadState = "queued" | "uploading" | "checking" | "accepted" | "rejected"

isAccepted

isAccepted(file, acceptedFileTypes) is re-exported from File Trigger, so you can run the same type check on files you add yourself.

FileUploadItemLabels

KeyDefault
queued"Waiting"
uploading"Uploading"
checking"Checking"
accepted"Accepted"
rejected"Rejected"
cancel"Cancel upload"
remove"Remove"
replace"Replace"
dropzone"Drop or paste a file to replace"