Forms Core Stable
FileUpload
A drop zone backed by a native file input, with per-file progress, JS-side validation and a removable file list
Preview
svelte
<script lang="ts">
import { FileUpload, type UploadFile } from "fancy-ui-svelte";
let files = $state<UploadFile[]>([]);
</script>
<div class="w-full max-w-sm">
<FileUpload bind:files hint="PNG, SVG — 4 MB max" />
</div>Installation
pnpm add fancy-ui-svelte
import { FileUpload } from 'fancy-ui-svelte'
Usage
svelte
<script lang="ts">
import { FileUpload } from 'fancy-ui-svelte';
</script>
<FileUpload />Examples
Basic Usage
svelte
<script lang="ts">
import { FileUpload, type UploadFile } from "$lib/fancy-ui/file-upload";
let files = $state<UploadFile[]>([]);
</script>
<div class="w-full max-w-sm">
<FileUpload bind:files hint="PNG, SVG — 4 MB max" />
</div>Multiple, With Progress
Driving status/progress from an onFilesChange callback with a simulated upload.
svelte
<script lang="ts">
// Demonstrates the intended shape of a real upload: FileUpload only draws
// whatever status/progress each row currently holds, so driving it is a
// matter of writing back into the bound array as an upload proceeds. This
// fakes the network with a timer instead of an actual request.
//
// Every write below goes through the bound `files` array itself, found by
// id — never through the row object `onFilesChange` hands back. That
// object is a plain snapshot; mutating it directly doesn't reach the
// reactive state Svelte is actually rendering from, so the bar never
// visibly moves even though the data technically changed.
import { FileUpload, type UploadFile } from "$lib/fancy-ui/file-upload";
let files = $state<UploadFile[]>([]);
function fakeUpload(id: string) {
const timer = setInterval(() => {
const entry = files.find((f) => f.id === id);
if (!entry || entry.progress === null) {
clearInterval(timer);
return;
}
entry.progress = Math.min(100, entry.progress + 10);
if (entry.progress >= 100) {
entry.status = "done";
clearInterval(timer);
}
}, 200);
}
function onFilesChange(next: UploadFile[]) {
for (const added of next) {
if (added.status !== "pending") continue;
const entry = files.find((f) => f.id === added.id);
if (entry) {
entry.status = "uploading";
entry.progress = 0;
}
fakeUpload(added.id);
}
}
</script>
<div class="w-full max-w-sm">
<FileUpload bind:files {onFilesChange} multiple hint="Any file — upload speed is simulated" />
</div>Constraints
accept, maxSize and maxFiles all enforced in JS on both the picker and a drop.
svelte
<script lang="ts">
// accept, maxSize and maxFiles are all re-checked in JS, on both the
// picker and a drop — the input's own `accept` only filters what the OS
// picker shows, so a dropped file needs the same JS gate to be enforced
// at all.
import { FileUpload, type UploadFile } from "$lib/fancy-ui/file-upload";
let files = $state<UploadFile[]>([]);
</script>
<div class="w-full max-w-sm">
<FileUpload
bind:files
multiple
accept="image/png,image/svg+xml"
maxSize={4_000_000}
maxFiles={3}
hint="PNG, SVG — 4 MB max, up to 3 files"
/>
</div>With FormField
svelte
<script lang="ts">
// FormField ships from this same wave, built by a different component.
// It supplies controlId, aria-describedby and aria-invalid to FileUpload
// through context — FileUpload itself needs no id or invalid prop here.
import { FormField } from "$lib/fancy-ui/form-field";
import { FileUpload, type UploadFile } from "$lib/fancy-ui/file-upload";
let files = $state<UploadFile[]>([]);
const error = $derived(files.length === 0 ? "Add at least one file." : undefined);
</script>
<FormField label="Attachments" required {error} class="w-full max-w-sm">
<FileUpload bind:files multiple hint="PNG, SVG — 4 MB max" />
</FormField>Props
| Prop | Type | Default | Description |
|---|---|---|---|
files | UploadFile[] | [] | Selected files; bindable |
onFilesChange | (files: UploadFile[]) => void | - | Called with the new list on every change — a selection, a drop, or a removal |
accept | string | - | The input's native accept attribute; also enforced in JS since a dropped file bypasses it |
multiple | boolean | false | Allows more than one file per selection or drop |
maxSize | number | - | Maximum size per file, in bytes. A larger file is added with status "error" |
maxFiles | number | - | Maximum number of files the list may hold. Extra files are rejected, not added |
disabled | boolean | false | Blocks selecting, dropping and removing files |
required | boolean | false | Native required on the underlying input |
invalid | boolean | false | Drives the error border and aria-invalid |
id | string | - | Element id, applied to the underlying file input |
name | string | - | Native name on the underlying input |
label | string | - | Accessible name — for a control with no visible Label next to it |
hint | string | - | Constraint text under the drop zone, e.g. "PNG, SVG — 4 MB max" |
class | string | - | Additional CSS classes |
ref | HTMLInputElement | null | null | Bindable reference to the underlying file input |
Links
Related components
Au
Autocomplete
A free-text field with a portalled panel of matching suggestions — any typed value is valid; the panel only ever helps finish it faster.
Forms Stable
Ch
Checkbox
A tri-look checkbox — unchecked, checked, indeterminate and disabled — built on a native checkbox input
Forms Stable
Co
Combobox
A single-choice text field over a closed set of options — typing filters a portalled listbox, and a value outside the list can never be selected.
Forms Stable