Skip to content
omaris

Inputs

File Upload

A dropzone that takes files from all three places people have them: dropped, browsed, and — the one usually missing — pasted.

import { FileUpload } from 'omaris'
Learn

Examples

Basic

One bindable prop. Drop, browse or paste — a screenshot on the clipboard is ⌘V away — and dropped folders are walked for you.

<script lang="ts">	import { FileUpload, type UploadFile } from 'omaris';​	let files = $state<UploadFile[]>([]);</script>​<FileUpload bind:files class="max-w-xl" />

Rules

The limits are props, and the hint under the headline writes itself from whichever ones you set. A file that breaks one is named rather than dropped in silence.

<script lang="ts">	import { FileUpload, type UploadFile } from 'omaris';​	let files = $state<UploadFile[]>([]);</script>​<FileUpload bind:files accept="image/*,.pdf" max={5} maxSize={2 * 1000 * 1000} class="max-w-xl" />

Uploading

Give it an upload function and it becomes an uploader: progress per file, cancel while in flight, retry after a failure, and concurrency in front of the queue. Resolve with whatever the server said — it lands on item.result.

<script lang="ts">	import { FileUpload, type UploadFile } from 'omaris';​	let files = $state<UploadFile[]>([]);​	// Stands in for a real request so the demo has something to show.	function send(		item: UploadFile,		{ progress, signal }: { progress: (n: number) => void; signal: AbortSignal }	) {		return new Promise<{ url: string }>((resolve, reject) => {			let at = 0;			const tick = setInterval(() => {				at = Math.min(100, at + 6 + Math.random() * 10);				progress(at);				if (at === 100) {					clearInterval(tick);					resolve({ url: `/uploads/${item.name}` });				}			}, 200);			signal.addEventListener('abort', () => {				clearInterval(tick);				reject(new Error('aborted'));			});		});	}</script>​<FileUpload bind:files upload={send} concurrency={2} class="max-w-xl" />

Anywhere

dropAnywhere takes a drop on any part of the page and raises a veil to say so, which is what a document view or an inbox wants — the target is the screen, not a box on it.

<script lang="ts">	import { FileUpload, type UploadFile } from 'omaris';​	let files = $state<UploadFile[]>([]);</script>​<FileUpload	bind:files	dropAnywhere	variant="outlined"	size="sm"	tone="info"	label="Drop a file anywhere on this page"	class="max-w-xl"/>

Overridden

Every part is reachable: classes names the slots, class covers the root and wins last, and the copy is three plain strings. A height set here beats the padding the size variant ships.

<script lang="ts">	import { FileUpload, type UploadFile } from 'omaris';​	let files = $state<UploadFile[]>([]);</script>​<FileUpload	bind:files	multiple={false}	accept="image/*"	tone="tertiary"	variant="tonal"	label="Cover image"	hint="1600×900 or larger"	browseLabel="Choose"	class="max-w-sm"	classes={{		zone: 'h-52 rounded-shape-2xl',		title: 'text-tertiary',		item: 'rounded-full'	}}/>

When to use it

Use it for

  • Any place a person hands the app a file: attachments on a form, an import screen, a document drop, an avatar or cover image. preview="grid" shows images as a gallery before they are saved.
  • Screenshots. paste is on by default, so ⌘V drops the clipboard image into the list. This is the main reason to use a dropzone over a bare <input type="file">.
  • Uploads with feedback. Pass upload and each file gets a progress bar, a cancel while in flight and a retry after it fails.
  • A whole folder. The picker takes one with directory; a dropped folder is always walked.
  • A drop target that is the page rather than a box on it: dropAnywhere.

Not for

  • A stored file someone is editing → pick it here, then crop it in Image Cropper.
  • Files already on the server, like a list of attachments with download links → List or Table. This component owns files on their way in.
  • One short string of text → Text Field.
  • Reordering or triaging files across columns → Kanban.
  • A gallery to browse rather than edit → Image Viewer or Masonry.

Do

  • Bind files and treat it as the state. accept, max, maxSize and minSize are rules about what may enter it; drop, browse and paste all go through them.
  • Set accept even when the server also checks. It filters the picker as well as the drop.
  • Give name when the dropzone sits in a real form. The input underneath is the control, so it submits and honours required.
  • Handle onreject if a refusal needs logging or showing elsewhere. The zone already names the first one under itself for a few seconds.
  • Return something from upload: an id, a URL, the server's row. It is kept on the item as result, which is what the form submits.
  • Use bind:this for open(), start() when autoUpload is off, and clear().
  • Use preview="none" and the file snippet when the list belongs somewhere else on the page.

Don't

  • Use dropAnywhere twice on one page, or leave paste on for two dropzones. Two veils fight over one drop, and one ⌘V lands in both; give all but one paste="focus".
  • Set max without multiple. A single-file dropzone replaces rather than refuses, which is what a new avatar should do.
  • Hold object URLs from preview after removing a file. The component revokes them when a file goes and on unmount; copy the blob if you need it longer.
  • Rely on paste alone on a phone. Keep the browse button; browseLabel set to null removes it, and touch has no ⌘V.
  • Put a maxSize here and a different one on the server.

Quick reference

tone
  • primary (default)
  • secondary
  • tertiary
  • destructive
  • success
  • warning
  • info
variant
  • dashed (default)
  • outlined
  • tonal
  • ghost
size
  • sm
  • md (default)
  • lg
preview
  • list (default)
  • grid
  • none

How accepted files are shown under the zone.

API

FileUpload

A dropzone that takes files from all three places people have them: dropped, browsed, and — the one usually missing — pasted.

The smallest useful call is one attribute:

That already drags, drops, browses, pastes screenshots, walks dropped folders, previews images, shows sizes and lets each file be removed. Everything past it is opt-in: accept and maxSize to filter, max to cap, preview="grid" for a gallery, and an upload function to make it send the files itself, with per-file progress, cancel and retry.

With no upload, the component is a picker: files is the state and the surrounding form does the sending.

import { FileUpload } from 'omaris'
<FileUpload bind:files />
<FileUpload  accept="image/*,.pdf"  maxSize={5 * 1000 * 1000}  upload={async (item, { progress, signal }) => {    const body = new FormData();    body.set('file', item.file);    const res = await fetch('/api/upload', { method: 'POST', body, signal });    progress(100);    return await res.json();  }}/>

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
No description in the source yet.
zone
The drop target. group/zone so the artwork inside can react to the drag without any of it being wired up in JavaScript.
spotlight
The pointer-following glow. Pure decoration, hidden from a11y.
trigger
The label that makes the whole zone one big file picker. It sits above the artwork and the copy — those are positioned too, and a later sibling paints over an earlier one, so without the z-10 the picker only opens where the zone happens to be empty.
media
The stack of sheets that fans open when a drag comes over.
sheet
One sheet of that stack. The two behind take the surface rather than the tone, or the three of them read as one lumpy shape instead of as paper stacked on paper.
face
The front sheet — the one with the arrow on it.
glyph
The arrow drawn on it.
copy
No description in the source yet.
title
No description in the source yet.
description
No description in the source yet.
controls
The row holding the browse button and any actions.
alert
The rule that just got broken, under the zone.
summary
"3 files · 8.2 MB" and the clear-all button.
list
No description in the source yet.
item
One file.
thumb
The thumbnail, or the extension when there is nothing to show.
info
No description in the source yet.
name
No description in the source yet.
meta
No description in the source yet.
bar
The per-file progress bar; present only while it means something.
actions
The remove / cancel / retry cluster.
overlay
The full-window veil dropAnywhere raises.
overlayCard
The card inside that veil.

Props

files bindable

Defaults to []

UploadFile[]

The accepted files. Bindable, and the only state you need — every other prop is a rule about what may enter it.

accept
string

<input accept> grammar: image/*, .csv, application/pdf.

multiple

Defaults to true

boolean
max
number

Cap on how many files may be held at once.

maxSize
number

Largest file, in bytes. 5 * 1000 * 1000 is 5 MB.

minSize
number

Smallest file, in bytes — catches empty and truncated files.

directory

Defaults to false

boolean

Let the picker take a whole folder. Dropped folders always walk.

paste

Defaults to true

boolean | 'focus'

Take files from a paste. true listens on the window and skips pastes aimed at a text field elsewhere; 'focus' only listens while the dropzone itself has focus. false turns it off.

dropAnywhere

Defaults to false

boolean

Accept a drop anywhere on the page, with a full-window veil.

capture
'user' | 'environment'

On a phone, open the camera instead of the file browser.

name
string

Submitted with the surrounding form, like any <input name>.

required

Defaults to false

boolean
disabled

Defaults to false

boolean
upload
UploadFn

Sends a file. Give it one and the component becomes an uploader.

autoUpload

Defaults to true

boolean

Start uploading the moment a file is accepted.

concurrency

Defaults to 3

number

How many uploads run at once.

variant

Defaults to 'dashed'

FileUploadVariant
dashed
The default: a dashed outline that firms up under a drag.
outlined
A hairline card, for a dropzone that sits inside a form.
tonal
Filled with the tone's container — the loudest of the three.
ghost
No container at all until a drag arrives.
tone

Defaults to 'primary'

FileUploadTone
primary
secondary
tertiary
destructive
success
warning
info
size

Defaults to 'md'

FileUploadSize
sm
md
lg
preview

Defaults to 'list'

FileUploadPreview

list rows, grid tiles, or none to render the files yourself.

list
grid
none
label
string

The headline in the zone.

hint
string

The line under it. Defaults to a sentence built from the rules.

browseLabel

Defaults to 'Browse files'

string | null

Text of the browse button. null removes it.

onadd
(files: UploadFile[]) => void

Called with the files that were just accepted.

onreject
(rejections: UploadRejection[]) => void

Called with the files that were turned away, and why.

onchange
(files: UploadFile[]) => void

Called with the whole list after any change to it.

oncomplete
(files: UploadFile[]) => void

Called once nothing is left to upload.

class
string
classes
FileUploadClasses

Per-part Tailwind overrides. class still covers the root.

icon
Snippet

Replaces the fanning sheets.

title
Snippet

Replaces the headline.

description
Snippet

Replaces the line under it.

actions
Snippet

Extra controls beside the browse button.

file
Snippet<[FileUploadRowContext]>

Replaces the whole row or tile for one file.