# File Uploads
LiveSvelte provides `useLiveUpload` for integrating Phoenix LiveView's file upload system with Svelte components.
## Quick Example
**LiveView:**
```elixir
defmodule MyAppWeb.UploadLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:uploaded_files, [])
|> allow_upload(:avatar, accept: ~w(.jpg .png), max_entries: 1)}
end
def handle_event("validate", _params, socket) do
{:noreply, socket}
end
def handle_event("submit", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
dest = Path.join("priv/static/uploads", Path.basename(path))
File.cp!(path, dest)
{:ok, "/uploads/#{Path.basename(dest)}"}
end)
{:noreply,
socket
|> update(:uploaded_files, &(&1 ++ uploaded_files))
|> put_flash(:info, "Uploaded successfully!")}
end
def render(assigns) do
~H"""
<.svelte
name="AvatarUpload"
props={%{uploads: @uploads}}
socket={@socket}
/>
"""
end
end
```
**Svelte Component:**
```svelte
<!-- assets/svelte/AvatarUpload.svelte -->
<script>
import { useLiveUpload } from "live_svelte"
let { uploads } = $props()
const {
showFilePicker,
entries,
submit,
cancel,
clear,
sync
} = useLiveUpload(uploads.avatar, { changeEvent: "validate", submitEvent: "submit" })
// Keep the composable in sync when the server pushes updated upload config
$effect(() => sync(uploads.avatar))
</script>
<div
role="button"
tabindex="0"
onclick={showFilePicker}
onkeydown={(e) => e.key === "Enter" && showFilePicker()}
>
Click to select a file (or drag and drop)
</div>
{#each $entries as entry (entry.ref)}
<div>
<p>{entry.client_name}</p>
<!-- Progress bar -->
<progress value={entry.progress} max="100">{entry.progress}%</progress>
<!-- Validation errors -->
{#each entry.errors as error}
<p class="error">{error}</p>
{/each}
<button type="button" onclick={() => cancel(entry.ref)}>Remove</button>
</div>
{/each}
<button onclick={submit} disabled={$entries.length === 0}>Upload</button>
```
## The `useLiveUpload` Composable
```ts
import { useLiveUpload } from "live_svelte"
const { showFilePicker, entries, submit, cancel, clear, sync } = useLiveUpload(
uploads.avatar,
{ changeEvent: "validate", submitEvent: "submit" }
)
// Sync updated config from server on every render
$effect(() => sync(uploads.avatar))
```
The first argument is the **upload config object** for a specific upload field (e.g., `uploads.avatar`). Pass it directly — not as a getter function.
Call `sync(uploads.avatar)` in a `$effect` to keep the composable up-to-date whenever the server sends an updated config.
> `useLiveUpload` creates a hidden `<form>` and `<input type="file">` internally and appends them to the LiveView element. You do not need to add a form in your Svelte template.
### Options
```ts
interface UploadOptions {
changeEvent?: string // Server event for phx-change (validation). Optional.
submitEvent: string // Server event for phx-submit. REQUIRED.
}
```
## Return Values
| Value | Type | Description |
|-------|------|-------------|
| `showFilePicker()` | `() => void` | Opens the native file picker dialog |
| `addFiles(files)` | `(files: File[] \| DataTransfer) => void` | Enqueue files programmatically (for drag-drop) |
| `entries` | `Readable<UploadEntry[]>` | Reactive store of current upload entries. Use `$entries` in templates. |
| `progress` | `Readable<number>` | Overall upload progress 0–100 averaged across all entries |
| `valid` | `Readable<boolean>` | `true` when the upload config has no top-level errors |
| `submit()` | `() => void` | Dispatch a form submit event to trigger Phoenix upload |
| `cancel(ref?)` | `(ref?: string) => void` | Cancel entry by ref string, or all entries when called with no arg |
| `clear()` | `() => void` | Reset the hidden input to clear the file queue |
| `sync(config)` | `(config: UploadConfig) => void` | Merge updated config from server. Call in `$effect`. |
## Upload Entry Fields
Each entry in `entries` has:
| Field | Type | Description |
|-------|------|-------------|
| `ref` | `string` | Unique entry identifier |
| `client_name` | `string` | Original filename |
| `client_size` | `number` | File size in bytes |
| `client_type` | `string` | MIME type |
| `progress` | `number` | Upload progress (0–100) |
| `errors` | `string[]` | Validation error messages |
| `valid` | `boolean` | Whether entry passes validation |
| `done` | `boolean` | Whether upload is complete |
| `preflighted` | `boolean` | Whether Phoenix has acknowledged (preflighted) this entry |
## Drag and Drop
```svelte
<script>
import { useLiveUpload } from "live_svelte"
let { uploads } = $props()
const { entries, cancel, sync } = useLiveUpload(uploads.avatar, { submitEvent: "submit" })
$effect(() => sync(uploads.avatar))
let dragOver = $state(false)
</script>
<div
class={dragOver ? "drag-over" : ""}
ondragover={(e) => { e.preventDefault(); dragOver = true }}
ondragleave={() => { dragOver = false }}
ondrop={(e) => {
e.preventDefault()
dragOver = false
// Phoenix LiveView handles the drop via phx-drop-target
}}
phx-drop-target={uploads.avatar?.ref}
>
Drop files here
</div>
```
## Multiple Files
Configure `max_entries` on the LiveView side:
```elixir
allow_upload(:photos, accept: ~w(.jpg .png .gif), max_entries: 5)
```
The `entries` array in the component will reflect all selected files.
## Validation
File validation is configured with `allow_upload/3` options:
```elixir
allow_upload(:avatar,
accept: ~w(.jpg .png .webp),
max_entries: 1,
max_file_size: 10_000_000 # 10 MB
)
```
Validation errors appear in `entry.errors` as human-readable strings.
## Progress Tracking
Upload progress is automatically tracked per entry via `entry.progress` (0–100):
```svelte
{#each $entries as entry (entry.ref)}
<div class="upload-item">
<span>{entry.client_name}</span>
<div class="progress-bar" style="width: {entry.progress}%"></div>
{#if entry.done}
<span>✓ Complete</span>
{/if}
</div>
{/each}
```