# File Upload

Upload cards with validation, progress, cancellation, retry, and animated completion.

- Package: `@mielui/svelte`
- Component version: `1.1.0`
- Depends on Mielui components: button, tooltip, spinner
- Shared utilities: hugeicons-icon, utils.cn, utils.createContext, transition

## Install

~~~~sh
pnpm dlx @mielui/svelte add file-upload
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### Details

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Dropzone

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Item

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| item | { readonly id: string; readonly file: File; readonly status: "uploading" \| "complete" \| "error"; readonly progress?: number \| undefined; readonly error?: string \| undefined; readonly retryable: boolean; } | - | Yes | No | Upload entry from the Root's list. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### List

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[FileUploadEntry]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Preview

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Progress

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Remove

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| onkeydown | ((event: KeyboardEvent) => void) \| undefined | - | No | No | Called on key down, before the button handles the key itself. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when the button is activated. Loading and disabled buttons do not call it. |
| status | ButtonStatus \| undefined | - | No | No | Controlled visual state. Loading remains focusable and refuses activation. |
| disabled | boolean \| undefined | - | No | No | Prevents activation. A disabled link loses its destination and is skipped by Tab. |
| variant | ButtonVariant \| undefined | - | No | No | Visual style. Choose it for what the action means, not for decoration. |
| size | "sm" \| "md" \| "lg" \| "icon" \| undefined | - | No | No | Control height. `icon` is a square button for a single icon. |
| element | HTMLButtonElement \| HTMLAnchorElement \| undefined | - | No | No | Bindable reference to the rendered DOM element. Type is the union of the two possible element types -- narrow at the use site: |
| unstyled | boolean \| undefined | - | No | No | Skip the variant/size base classes and render with `class` alone. |
| loading | boolean \| undefined | - | No | No | Convenience alias for `status="loading"`. |
| labels | ButtonLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Retry

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| onkeydown | ((event: KeyboardEvent) => void) \| undefined | - | No | No | Called on key down, before the button handles the key itself. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when the button is activated. Loading and disabled buttons do not call it. |
| status | ButtonStatus \| undefined | - | No | No | Controlled visual state. Loading remains focusable and refuses activation. |
| disabled | boolean \| undefined | - | No | No | Prevents activation. A disabled link loses its destination and is skipped by Tab. |
| variant | ButtonVariant \| undefined | - | No | No | Visual style. Choose it for what the action means, not for decoration. |
| size | "sm" \| "md" \| "lg" \| "icon" \| undefined | - | No | No | Control height. `icon` is a square button for a single icon. |
| element | HTMLButtonElement \| HTMLAnchorElement \| undefined | - | No | No | Bindable reference to the rendered DOM element. Type is the union of the two possible element types -- narrow at the use site: |
| unstyled | boolean \| undefined | - | No | No | Skip the variant/size base classes and render with `class` alone. |
| loading | boolean \| undefined | - | No | No | Convenience alias for `status="loading"`. |
| labels | ButtonLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| labels | FileUploadLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |
| accept | string \| undefined | - | No | No | Accepted file types, written as for a file input. |
| maxSize | number \| undefined | - | No | No | Largest file size in bytes. |
| maxFiles | number \| undefined | - | No | No | Largest number of files. |
| disabled | boolean \| undefined | false | No | No | Prevents interaction and dims the control. |
| onUpload | (file: File, options: { signal: AbortSignal; onProgress: (percent: number) => void; }) => Promise<void> | - | Yes | No | Uploads one file. Report progress with `onProgress` and stop when `signal` aborts. |
| children | Snippet<[FileUploadSummary]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Status

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Trigger

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| onkeydown | ((event: KeyboardEvent) => void) \| undefined | - | No | No | Called on key down, before the button handles the key itself. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when the button is activated. Loading and disabled buttons do not call it. |
| status | ButtonStatus \| undefined | - | No | No | Controlled visual state. Loading remains focusable and refuses activation. |
| disabled | boolean \| undefined | - | No | No | Prevents activation. A disabled link loses its destination and is skipped by Tab. |
| variant | ButtonVariant \| undefined | - | No | No | Visual style. Choose it for what the action means, not for decoration. |
| size | "sm" \| "md" \| "lg" \| "icon" \| undefined | - | No | No | Control height. `icon` is a square button for a single icon. |
| element | HTMLButtonElement \| HTMLAnchorElement \| undefined | - | No | No | Bindable reference to the rendered DOM element. Type is the union of the two possible element types -- narrow at the use site: |
| unstyled | boolean \| undefined | - | No | No | Skip the variant/size base classes and render with `class` alone. |
| loading | boolean \| undefined | - | No | No | Convenience alias for `status="loading"`. |
| labels | ButtonLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Public types

~~~~ts
import type { ButtonProps } from '@mielui/svelte/components/button';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import Root from './file-upload.svelte';
import Details from './file-upload-details.svelte';
import Dropzone from './file-upload-dropzone.svelte';
import Item from './file-upload-item.svelte';
import List from './file-upload-list.svelte';
import Preview from './file-upload-preview.svelte';
import Progress from './file-upload-progress.svelte';
import Remove from './file-upload-remove.svelte';
import Retry from './file-upload-retry.svelte';
import Status from './file-upload-status.svelte';
import Trigger from './file-upload-trigger.svelte';

export type FileUploadEntry = {
    readonly id: string;
    readonly file: File;
    readonly status: 'uploading' | 'complete' | 'error';
    readonly progress?: number;
    readonly error?: string;
    readonly retryable: boolean;
};
export type FileUploadSummary = {
    items: readonly FileUploadEntry[];
    total: number;
    uploading: number;
    complete: number;
};
export type FileUploadLabels = {
    /** Heading in the empty dropzone. */
    dropzoneTitle?: string;
    /** Supporting line in the empty dropzone. */
    dropzoneDescription?: string;
    /** Trigger text while no file is selected. */
    choose?: string;
    /** Trigger text once files are selected. */
    addMore?: string;
    /** Accessible name of the file list. */
    list?: string;
    /** Status while a file uploads. */
    uploading?: string;
    /** Status once a file has uploaded. */
    uploaded?: string;
    /** Accessible name of a file's progress bar. */
    uploadingFile?: (name: string) => string;
    /** Name of the button that removes a file. */
    remove?: string;
    /** Name of the button that cancels an upload in progress. */
    cancel?: string;
    /** Name of the button that retries a failed upload. */
    retry?: string;
    /** Error for a file whose type `accept` rejects. */
    typeRejected?: string;
    /** Error for a file larger than `maxSize`. */
    tooLarge?: (megabytes: number) => string;
    /** Error for a file that is already selected. */
    duplicate?: string;
    /** Error for a file beyond `maxFiles`. */
    tooMany?: (maxFiles: number) => string;
    /** Shown on a file whose upload failed without an error message. */
    uploadFailed?: string;
};

export type FileUploadProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
    /**
     * Overrides the built-in text and accessible names. Every key is optional and English is the
     * fallback.
     */
    labels?: FileUploadLabels;
    /** Accepted file types, written as for a file input. */
    accept?: string;
    /** Largest file size in bytes. */
    maxSize?: number;
    /** Largest number of files. */
    maxFiles?: number;
    /** Prevents interaction and dims the control. */
    disabled?: boolean;
    /** Uploads one file. Report progress with `onProgress` and stop when `signal` aborts. */
    onUpload: (
        file: File,
        options: {
            signal: AbortSignal;
            onProgress: (percent: number) => void;
        }
    ) => Promise<void>;
    /** Content rendered inside. */
    children?: Snippet<[FileUploadSummary]>;
};
export type FileUploadPartProps = HTMLAttributes<HTMLDivElement>;
export type FileUploadItemProps = FileUploadPartProps & { item: FileUploadEntry };
export type FileUploadListProps = Omit<HTMLAttributes<HTMLUListElement>, 'children'> & {
    /** Content rendered inside. */
    children?: Snippet<[FileUploadEntry]>;
};
export type FileUploadButtonProps = Omit<
    Extract<ButtonProps, { href?: undefined }>,
    'href' | 'type'
>;
export { Details, Dropzone, Item, List, Preview, Progress, Remove, Retry, Root, Status, Trigger };
~~~~

## Examples

### Disabled

~~~~svelte
<script lang="ts">
    import * as FileUpload from '@mielui/svelte/components/file-upload';
    import { Switch } from '@mielui/svelte/components/switch';

    let locked = $state(true);

    async function keepLocal() {
        await new Promise((resolve) => setTimeout(resolve, 600));
    }
</script>

<div class="flex w-full max-w-md flex-col gap-4">
    <Switch bind:checked={locked} label="Lock uploads" />
    <FileUpload.Root disabled={locked} maxFiles={3} onUpload={keepLocal} class="w-full">
        <FileUpload.Dropzone>
            <p class="text-sm font-medium">Attachments</p>
            <p class="text-sm text-foreground-muted">
                {locked ? 'Uploads are locked.' : 'Drop files here or choose them.'}
            </p>
            <FileUpload.Trigger variant="outline" size="sm">Choose files</FileUpload.Trigger>
        </FileUpload.Dropzone>
    </FileUpload.Root>
</div>
~~~~

### Hero

~~~~svelte
<script lang="ts">
    import * as FileUpload from '@mielui/svelte/components/file-upload';
    import { Switch } from '@mielui/svelte/components/switch';

    type UploadOptions = Parameters<FileUpload.FileUploadProps['onUpload']>[1];

    let failNext = $state(false);

    async function upload(_file: File, { signal, onProgress }: UploadOptions) {
        const shouldFail = failNext;
        failNext = false;

        await new Promise<void>((resolve, reject) => {
            let progress = 0;

            function cancel() {
                clearInterval(timer);
                reject(new DOMException('Upload cancelled', 'AbortError'));
            }

            const timer = setInterval(() => {
                progress += 10;
                onProgress(progress);

                if (progress < 100) {
                    return;
                }

                clearInterval(timer);
                signal.removeEventListener('abort', cancel);

                if (shouldFail) {
                    reject(new Error('Connection interrupted. Try uploading again.'));
                } else {
                    resolve();
                }
            }, 180);

            signal.addEventListener('abort', cancel, {
                once: true
            });
        });
    }
</script>

<div class="flex w-full max-w-md flex-col gap-4">
    <div class="flex flex-col gap-1">
        <p class="text-sm font-medium">Project files</p>
        <p class="text-sm text-foreground-muted">Images and PDFs. Up to 3 files, 10 MB each.</p>
    </div>
    <FileUpload.Root
        accept="image/*,.pdf"
        maxFiles={3}
        maxSize={10 * 1024 * 1024}
        onUpload={upload}
    />
    <Switch bind:checked={failNext} label="Fail the next upload" />
    <p class="text-xs text-foreground-muted">Demo only. Files stay on your device.</p>
</div>
~~~~

### Single

~~~~svelte
<script lang="ts">
    import * as FileUpload from '@mielui/svelte/components/file-upload';

    type UploadOptions = Parameters<FileUpload.FileUploadProps['onUpload']>[1];

    async function prepare(_file: File, { signal }: UploadOptions) {
        signal.throwIfAborted();
        await Promise.resolve();
    }

    function statusText(item: FileUpload.FileUploadEntry) {
        if (item.status === 'error') {
            return item.error;
        }

        if (item.status === 'complete') {
            return 'Ready on this device';
        }

        return 'Preparing photo…';
    }
</script>

<FileUpload.Root
    accept="image/png,image/jpeg,image/webp"
    maxFiles={1}
    maxSize={5 * 1024 * 1024}
    onUpload={prepare}
    class="w-full max-w-md"
>
    {#snippet children({ items })}
        {#if items.length === 0}
            <FileUpload.Dropzone>
                <p class="text-sm font-medium">Profile photo</p>
                <p class="text-sm text-foreground-muted">One PNG, JPEG, or WebP, up to 5 MB.</p>
                <FileUpload.Trigger variant="outline">Choose photo</FileUpload.Trigger>
            </FileUpload.Dropzone>
        {:else}
            <FileUpload.List>
                {#snippet children(item)}
                    <FileUpload.Item {item}>
                        <FileUpload.Preview />
                        <div class="min-w-0 flex-1 space-y-2">
                            <FileUpload.Details />
                            <FileUpload.Status>{statusText(item)}</FileUpload.Status>
                        </div>
                        <FileUpload.Remove size="sm" variant="ghost">Remove</FileUpload.Remove>
                    </FileUpload.Item>
                {/snippet}
            </FileUpload.List>
            <p class="text-xs text-foreground-muted">Remove the photo to choose a replacement.</p>
        {/if}
        <p class="text-xs text-foreground-muted">
            Local preview only. This example does not upload your file.
        </p>
    {/snippet}
</FileUpload.Root>
~~~~

For the rendered reference, visit [/docs/components/file-upload](/docs/components/file-upload).
