# FileUpload

> A drop zone backed by a native file input, with per-file progress, JS-side validation and a removable file list.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: Forms
- Tags: file-upload, upload, drag-and-drop, form, validation, progress
- Docs: https://fancy-ui.rama.app/docs/components/file-upload
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/file-upload

## Import

```svelte
<script lang="ts">
  import { FileUpload } from 'fancy-ui-svelte';
</script>
```

Setup (once per project): `npm install fancy-ui-svelte`, then add `@import "fancy-ui-svelte/tailwind.css";` after `@import "tailwindcss";` in your main CSS file. Full guide: https://fancy-ui.rama.app/llms.txt

React: `import { FileUpload } from 'fancy-ui-react';` (same prop names; `class` becomes `className`, snippets become `ReactNode`).

## 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 |
| `sound` | `boolean` | `false` | Plays select on an accepted selection or drop and error on a rejection, once the user has enabled sound |

## Examples

### Basic Usage

```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 sound 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 "fancy-ui-svelte";

	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
		sound
		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 "fancy-ui-svelte";

	let files = $state<UploadFile[]>([]);
</script>

<div class="w-full max-w-sm">
	<FileUpload
		sound
		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 "fancy-ui-svelte";
	import { FileUpload, type UploadFile } from "fancy-ui-svelte";

	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 sound bind:files multiple hint="PNG, SVG — 4 MB max" />
</FormField>
```
