File upload

Choose files and watch them arrive: a FileUpload object with five parts, three item states, five zone states — and no uploading of its own.

Demo

Three parts take props worth turning: the zone and the two items. Pick one, change its props, and copy the result.

or drag and drop

SVG, PNG, JPG or GIF (max. 800x400px)

Component
import { FileUpload } from "@/components/application/file-upload/file-upload-base"; // Wire onDropFiles to your upload; see the Code tab for the two // rejection handlers, which are what turn the invalid state on. <FileUpload.DropZone onDropFiles={upload} />

The kit uploads nothing

DropZone hands accepted files to onDropFiles once and stops there; the items draw the progress you pass and advance nothing. The playground has no upload behind it, so dropping a file on its zone does nothing visible, and the copied snippet names the handler you have to wire. The contextual demo below runs a real queue on a timer.

Contextual demo

An uploader is a dialog or a settings row most of the time: three of them here, with a queue that advances on its own.

Six consumers already ship it

The kit’s own file-upload-modal renders the zone and a queue of bar items, file-upload-menu the zone and three fill items, new-project-modal a single-image zone, form-02-modal and profile-settings-modal the zone beside an avatar, the first with its icon hidden, and new-message-filled-modal only getReadableFileSize, for its attachment chips. The fixtures on this page are the slideout’s three files.

The family

One FileUpload object with five parts plus a size formatter, from one file. The zone and both items are named exports.

The components in this family, with their descriptions and import paths
ComponentDescriptionImport from
FileUpload.RootA div with flex-col gap-4: 16px between the drop zone and the list. Spreads every div prop.@/​components/​application/​file-upload/​file-upload-base
FileUpload.ListA ul with flex-col gap-3, wrapping its children in motion’s AnimatePresence so an item removed from the list lets the rest slide up. Spreads every ul prop.@/​components/​application/​file-upload/​file-upload-base
FileUpload.DropZoneThe target: a featured icon, a Click to upload button over a hidden file input, or drag and drop, and a hint. Owns drag-over and invalid state, checks accept and maxSize, and hands accepted files to onDropFiles. Also exported as FileUploadDropZone.@/​components/​application/​file-upload/​file-upload-base
FileUpload.ListItemProgressBarOne queued file as a motion.li: file icon, name, done of total, a status word, a delete button, and the base ProgressBar with its percentage on the right. Failed swaps the bar for Try again. Also exported as FileListItemProgressBar.@/​components/​application/​file-upload/​file-upload-base
FileUpload.ListItemProgressFillThe same file with the progress as a fill sliding across the card behind the text, and the percentage in the supporting row. Also exported as FileListItemProgressFill.@/​components/​application/​file-upload/​file-upload-base
getReadableFileSizebytes → “200 KB”, floored to a whole unit from B up to YB, and “0 KB” for 0. What both items print, and what the new message modal uses for its attachment chips.@/​components/​application/​file-upload/​file-upload-base

Built from

Seven pieces make up the family; six have their own docs page.

Component
Where it’s used here
ButtonClick to upload, as color link-color at size md, which opens the picker by clicking the hidden input; and Try again, as link-destructive at size sm, on a failed item.
ButtonUtilityThe delete button on both items: color tertiary, size xs, Trash01, tooltip Delete, and since 2026-08-23 an aria-label of Delete {name} that wins over the tooltip’s.
ProgressBarThe bar item’s track, labelPosition right with min 0 and max 100, named {name} upload through the aria-label prop the base component gained on 2026-08-23 for this family. The fill item draws its own progressbar div instead.
FeaturedIconThe zone’s 40px UploadCloud02 tile: color gray, theme modern, size md, dimmed to opacity 50 when disabled.
FileIconFrom @untitledui/file-icons, twice per item: a light and a dark theme, one hidden per color scheme, at 40px, with the item’s type and fileIconVariant.
@untitledui/iconsUploadCloud02 in the zone and the uploading status, CheckCircle for complete, XCircle for failed, Trash01 on the delete button.

The drop zone

The zone as consumers pass it: default, single-image, disabled, rejecting. The sheet’s Hover is under States; all live.

or drag and drop

SVG, PNG, JPG or GIF (max. 800x400px)

Default: the kit’s own hint, “SVG, PNG, JPG or GIF (max. 800x400px)”
or drag and drop

SVG, PNG, or JPG (max. 800×400px)

hint, accept, and allowsMultiple={false}: a single-image zone, as new-project-modal and the section headers preview pass it; the kit’s two avatar modals pass none of the three
or drag and drop

SVG, PNG, JPG or GIF (max. 800x400px)

isDisabled: bg-secondary, the icon at opacity 50, and drag events ignored
or drag and drop

PNG only, up to 10 KB

0 files rejected so far

accept and maxSize with both rejection handlers: drop anything but a small PNG to see the hint turn error

126px tall, read from the source

py-4 above and below, the 40px featured icon, gap-3, then a 42px text block: the 20px link row, a 4px gap, and the 18px hint. Every one of those is the sheet’s value in the 2026-08-23 parity read, as are the 12px radius, the 1px ring-secondary, the 2px ring-brand on drag-over, and the disabled fill. The zone is a block-level div with no width of its own, so it fills its container; the sheet’s 512 and 343px are its Desktop and Mobile artboards, not a cap.

Two copies, one breakpoint

Read from the source: below md the button reads “Click to upload and attach files” and the span “or drag and drop” is hidden, since a phone has nothing to drag. The sheet’s Mobile symbol keeps “or drag and drop” at 343px, so this is a recorded divergence the kit keeps on purpose; it also means the button’s accessible name changes with the viewport.

The two items

Progress bar and Progress fill, each at In progress, Complete, and Error. Every cell is an li inside the kit’s List.

ListItemProgressBar
  • Tech design requirements.pdf

    40 KB of 200 KB

    Uploading...

    20%
progress={20}: Uploading..., 40 KB of 200 KB, the bar at 20%
  • Tech design requirements.pdf

    200 KB of 200 KB

    Complete

    100%
progress={100}: Complete, the bar full
  • Tech design requirements.pdf

    0 KB of 200 KB

    Failed

failed: Failed, a 2px ring-error, 0 KB of 200 KB, and Try again in place of the bar
ListItemProgressFill
  • Tech design requirements.pdf

    200 KB

    40%

progress={40}: the cloud icon beside 40%, 200 KB, and the fill at 40%; no status word
  • Tech design requirements.pdf

    200 KB

    100%

progress={100}: the check icon beside 100%, and the fill faded to opacity 0; no status word
  • Tech design requirements.pdf

    Upload failed, please try again.

failed: a 2px ring-error, “Upload failed, please try again.”, Try again, and no fill element at all

98px and 74px, read from the source

Both cards are p-4 around a 40px icon and a text block of a 20px name, mt-0.5, and a 20px supporting row, 74px in all. The bar item adds mt-1 and the 20px progress row, 98px. The sheet draws both heights, every padding and gap, the 12px divider, the 16px status icons, and the 28px delete button 8px from the top and end edges, which is -mt-2 -me-2 inside p-4. The fill is bg-secondary and bleeds 1px over the ring since 2026-08-23; at 100 it fades to opacity 0 rather than leaving.

The sizes are the kit’s arithmetic

getReadableFileSize floors to a whole unit, so the 204,800 byte fixture reads “200 KB”, 20% of it “40 KB”, and the slideout’s 4,404,019 byte prototype “4 MB”. The bar item prints done of total since 2026-08-23, “40 KB of 200 KB”, with 0 when failed, the sheet’s own line; the fill item prints the total and a percent, which the sheet also draws without a total. Read from the source.

A queue

Root holds the zone and a List of three. Delete is the list’s one behavior; removal animates, off under reduced motion.

or drag and drop

SVG, PNG, JPG or GIF (max. 800x400px)

  • Tech design requirements.pdf

    200 KB of 200 KB

    Complete

    100%
  • Dashboard recording.mp4

    6 MB of 16 MB

    Uploading...

    40%
  • Dashboard prototype FINAL.fig

    3 MB of 4 MB

    Uploading...

    80%
Root, DropZone, and a List of three ListItemProgressBar: the kit’s file upload modal
or drag and drop

SVG, PNG, JPG or GIF (max. 800x400px)

  • Tech design requirements.pdf

    200 KB

    100%

  • Dashboard recording.mp4

    16 MB

    40%

  • Dashboard prototype FINAL.fig

    4 MB

    80%

The same with ListItemProgressFill: the kit’s file upload slideout

16px then 12px

Root is flex-col gap-4 and List is flex-col gap-3, the sheet’s 16px between the zone and the list and 12px between items. Every consumer in this repo repeats both strings through className, which merges after the kit’s own and is redundant; two of the three Root consumers also add the padding or margin the prop is for. The list’s AnimatePresence carries initial={false} and the items define no exit, so a deleted item leaves at once and only the survivors animate.

States

The zone has five states and one prop, isDisabled; an item has three, all props. Only the buttons have hover treatments.

or drag and drop

SVG, PNG, JPG or GIF (max. 800x400px)

Focus: tab into the cell. The first stop is the hidden input, and the zone draws its 2px ring-brand while it has focus; the second is the link
or drag and drop

Drag a file over this zone

Drag-over: the same 2px ring-brand while a file is over the zone. Internal state, with no prop; the sheet calls it State=Hover
or drag and drop

PNG only, up to 10 KB

0 files rejected so far

Invalid: the hint in text-error-primary, aria-invalid on the input, and a status line. Drop a wrong file here
or drag and drop

SVG, PNG, JPG or GIF (max. 800x400px)

Disabled: isDisabled, the one prop state

Invalid needs a handler

Read from the source: processFiles sets the invalid state inside the branch that calls onSizeLimitExceed or onDropUnacceptedFiles, and that branch runs only when the prop is a function. A zone with accept and no handler discards a wrong file without a word. The two rejecting cells on this page pass both, and count what reached them; the don’t card on the Guidelines tab shows the silent version beside it.

An item’s state is two props

failed wins; otherwise progress === 100 is complete and anything else is uploading, a strict equality read from the source, so 99.5 and 100.4 both still read as uploading. The three cells under each item above are the whole state space; the sheet draws the same three as In progress, Complete, and Error. The words belong to the bar item alone: the fill item shows the check or cloud icon beside its percent, and when failed the sentence with no icon.