FileUpload
Drag-and-drop file upload with validation, image previews, progress tracking, and animated file list. Supports multiple files, paste from clipboard, and custom dropzone designs.
Playground
<script lang="ts">
import { FileUpload } from '@urbicon-ui/blocks';
let files = $state([]);
</script>
<FileUpload
bind:files
intent="primary"
/>01 Examples
Basic upload
<script lang="ts">
import { FileUpload, type FileUploadFile } from '@urbicon-ui/blocks';
let files = $state<FileUploadFile[]>([]);
</script>
<div class="max-w-md">
<FileUpload
bind:files
title="Drop a file here, or click"
description="Beliebiger Dateityp, max. 10 MB"
maxFileSize={10 * 1024 * 1024}
/>
{#if files.length > 0}
<div class="bg-surface-elevated border-border-subtle mt-3 rounded-lg border p-3">
<p class="text-text-secondary text-sm">
<span class="text-text-primary font-medium">{files.length}</span> Datei(en) ausgewaehlt
</p>
</div>
{/if}
</div>
Images with a preview
<script lang="ts">
import { FileUpload, IMAGE_MIME_TYPES, type FileUploadFile } from '@urbicon-ui/blocks';
let files = $state<FileUploadFile[]>([]);
</script>
<div class="max-w-lg">
<FileUpload
bind:files
accept={IMAGE_MIME_TYPES}
multiple
maxFiles={8}
maxFileSize={5 * 1024 * 1024}
title="Bilder hochladen"
description="PNG, JPG, WebP, GIF, AVIF — max. 5 MB pro Bild"
/>
<!-- Custom grid preview below the component -->
{#if files.length > 0}
<div class="mt-4 grid grid-cols-4 gap-2">
{#each files as entry (entry.id)}
{#if entry.preview}
<div
class="bg-surface-base border-border-subtle group relative aspect-square overflow-hidden rounded-lg border"
>
<img
src={entry.preview}
alt={entry.file.name}
class="size-full object-cover transition-transform duration-[var(--blocks-duration-fast)] group-hover:scale-105"
/>
<div
class="from-surface-inverted/60 absolute inset-x-0 bottom-0 bg-gradient-to-t to-transparent p-2"
>
<p class="text-text-inverted truncate text-xs">{entry.file.name}</p>
</div>
</div>
{/if}
{/each}
</div>
{/if}
</div>
Validation with feedback
<script lang="ts">
import { FileUpload, Alert, type FileUploadFile, type FileRejection } from '@urbicon-ui/blocks';
let files = $state<FileUploadFile[]>([]);
let rejections = $state<FileRejection[]>([]);
function handleReject(r: FileRejection[]) {
rejections = r;
setTimeout(() => (rejections = []), 5000);
}
</script>
<div class="max-w-md space-y-3">
<FileUpload
bind:files
accept={['.pdf', '.docx', '.xlsx']}
maxFileSize={2 * 1024 * 1024}
maxFiles={3}
multiple
title="Dokumente hochladen"
description="Nur PDF, DOCX, XLSX — max. 2 MB, max. 3 Dateien"
onFileReject={handleReject}
/>
{#if rejections.length > 0}
<Alert intent="danger" variant="soft" dismissible onDismiss={() => (rejections = [])}>
<div class="space-y-1">
{#each rejections as rejection (rejection.file.name)}
<p class="text-sm">
<span class="font-medium">{rejection.file.name}:</span>
{rejection.errors.map((e) => e.message).join(', ')}
</p>
{/each}
</div>
</Alert>
{/if}
</div>
Upload progress
<script lang="ts">
import { FileUpload, Button, type FileUploadFile } from '@urbicon-ui/blocks';
let files = $state<FileUploadFile[]>([]);
let uploading = $state(false);
function simulateUpload() {
if (files.length === 0 || uploading) return;
uploading = true;
const pending = files.filter((f) => f.status === 'pending');
if (pending.length === 0) {
uploading = false;
return;
}
let idx = 0;
function uploadNext() {
if (idx >= pending.length) {
uploading = false;
return;
}
const entry = pending[idx];
entry.status = 'uploading';
entry.progress = 0;
files = [...files];
const interval = setInterval(() => {
entry.progress = Math.min((entry.progress ?? 0) + Math.random() * 15 + 5, 100);
files = [...files];
if (entry.progress >= 100) {
clearInterval(interval);
entry.status = Math.random() > 0.15 ? 'complete' : 'error';
if (entry.status === 'error') {
entry.errors = [{ code: 'CUSTOM', message: 'Netzwerkfehler beim Upload' }];
}
files = [...files];
idx++;
setTimeout(uploadNext, 300);
}
}, 200);
}
uploadNext();
}
</script>
<div class="max-w-md space-y-3">
<FileUpload
bind:files
multiple
maxFiles={4}
title="Choose files to upload"
description="Klicke 'Upload starten' nach der Auswahl"
/>
{#if files.length > 0}
<div class="flex justify-end">
<Button
intent="primary"
size="sm"
onclick={simulateUpload}
loading={uploading}
disabled={uploading || files.every((f) => f.status !== 'pending')}
>
Upload starten
</Button>
</div>
{/if}
</div>
02 Customization
Custom Dropzone Design
children snippet replaces the dropzone contents wholesale — here a gradient ground, an icon of its own and a call to action, with slotClasses handling the frame.Design-Assets hochladen
Bilder und PDFs, max. 8 MB pro Datei
<script lang="ts">
import { FileUpload, UploadCloudIcon, type FileUploadFile } from '@urbicon-ui/blocks';
let files = $state<FileUploadFile[]>([]);
</script>
<div class="max-w-md">
<FileUpload
bind:files
multiple
maxFiles={6}
accept={['image/*', '.pdf']}
maxFileSize={8 * 1024 * 1024}
slotClasses={{
dropzone:
'bg-gradient-to-br from-primary/5 via-surface-base to-secondary/5 border-primary/30 hover:border-primary hover:from-primary/10 hover:to-secondary/10 rounded-xl py-12'
}}
>
<div class="flex flex-col items-center gap-3 text-center">
<div class="bg-primary/10 flex size-14 items-center justify-center rounded-full">
<UploadCloudIcon strokeWidth={1.5} class="text-primary size-7" />
</div>
<div>
<p class="text-text-primary font-semibold">Design-Assets hochladen</p>
<p class="text-text-tertiary mt-1 text-sm">Bilder und PDFs, max. 8 MB pro Datei</p>
</div>
<div
class="bg-primary text-text-on-primary mt-1 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium"
>
Choose files
</div>
</div>
</FileUpload>
</div>
03 Accessibility
ARIA and roles
The dropzone is a role="button" with tabindex="0". The file list is a role="list" with aria-live="polite", so a screen reader hears every
change without being asked; each entry is a role="listitem". The real <input type="file"> stays in the DOM, visually hidden,
because nothing beats it for compatibility.
Keyboard
Enter or Space on the dropzone opens the native file dialog. Tab moves between the dropzone, the file items and their remove buttons. Focus rings use focus-visible:, so they appear for the keyboard only.
Drag states
The dropzone's data-state moves between idle, accept and reject, which is enough to style the whole
interaction in unstyled mode from CSS alone. Colour, scale
and shadow tell the reader whether what they are dragging will be taken.
Document Drop Prevention
On by default through preventDocumentDrop: a file
dropped anywhere but the dropzone does not open in the browser. Without it, a near-miss
navigates away from the page and takes unsaved work with it.
04 API Reference
Prop | Type | Default | Description | |
|---|---|---|---|---|
accept | string | string[] | — | Accepted MIME types or file extensions (e.g. 'image/*', '.pdf'). | |
allowDrop | boolean | true | Enable drag-and-drop. | |
allowPaste | boolean | false | Enable paste from clipboard. | |
children | Snippet | — | Default slot for fully custom dropzone content. | |
class | string | — | Additional CSS class for the root element. | |
description | string | — | Dropzone description text (accepted types, limits). | |
disabled | boolean | — | Disable all interaction. | |
dropzoneIcon | Snippet | — | Custom dropzone icon snippet. | |
file | File | null | — | Convenience binding for single-file uses. Two-way:
- Reads as files[0]?.file ?? null.
- Setting to a File replaces the current selection (object URLs are
revoked, no validation re-runs — assumes the caller already validated).
- Setting to null clears the list.
Recommended when maxFiles === 1 (e.g. logo / avatar uploads). Use
bind:files instead when you need progress, errors, or status metadata. | |
fileItem | Snippet<[FileItemContext]> | — | Custom file item renderer. Receives file entry and remove callback. | |
files | FileUploadFile[] | — | Current file list. Supports two-way binding. | |
intent variant | neutralprimary | neutral | Controls the color theme and semantic meaning of the FileUpload. Affects the overall appearance and user perception. Available options: neutral, primary. | |
maxFiles | number | — | Maximum number of files. | |
maxFileSize | number | — | Maximum file size in bytes. | |
minFileSize | number | — | Minimum file size in bytes. | |
mint | MintProp | 'none' | Micro-interaction preset applied to the dropzone. Only applies while not disabled. | |
multiple | boolean | — | Allow selecting multiple files. | |
name | string | — | Shared name for native form submission. When set, the underlying
hidden <input type="file"> carries the current file list — including
files added via drag/drop, paste, or programmatic bind:files, not
just files picked through the file dialog. Submits as a File[] under
{name} in the FormData payload. | |
onFileAccept | (files: FileUploadFile[]) => void | — | Fires when valid files are accepted. | |
onFileReject | (rejections: FileRejection[]) => void | — | Fires when files are rejected by validation. | |
onFileRemove | (file: FileUploadFile) => void | — | Fires when a file is removed. | |
onFilesChange | (files: FileUploadFile[]) => void | — | Fires when the file list changes (add or remove). | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ FileUpload: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
preventDocumentDrop | boolean | true | Prevent browser navigation when files are dropped outside the zone. | |
required | boolean | — | Mark as required for form validation. | |
size variant | lgmdsm | md | Controls the dimensions, padding, and text size of the FileUpload. Affects the component's physical footprint. Available options: lg, md, sm. | |
slotClasses | Partial<Record<FileUploadSlots, string>> | — | Per-slot class overrides. | |
title | string | — | Dropzone title text. | |
unstyled | boolean | — | Strip all default styles. | |
validate | (file: File) => FileUploadError[] | null | — | Custom validation function. Return errors array or null. | |
...FileUploadVariants variant | VariantProps | — | Styling variants from FileUploadVariants | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children') |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
FileItemContext | interface | helper | 0 | — | |
FileUploadSlotName | type | helper | 0 | Slot names for slotClasses — derived from the tv() config (single source of truth). | |
FileUploadProps | interface | props | 0 | — | |
MintProp | type | helper | 1 | — | |
FileUploadSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. | |
FileUploadVariants | type | variant | 0 | — | |
FileIntakeRejection | interface | helper | 0 | — | |
FileIntakeError | interface | helper | 0 | — | |
FileIntakeErrorCode | type | helper | 0 | — | |
FileIntakeEntry | interface | helper | 0 | — | |
FileIntakeStatus | type | helper | 0 | — | |
MintName | type | helper | 0 | A mint name: a built-in (autocompleted), 'none' to disable, or any
consumer-registered name. (string & {}) keeps the registry open — a
custom name still type-checks, it just isn't suggested. A typo therefore
also still compiles (it resolves like an unregistered custom name and
warns at runtime); the union buys completion and docs, not validation. | |
MintConfig | interface | helper | 0 | — | |
SlotNames | type | helper | 0 | Extracts the slot-name union from a slotted tv() config function — the
companion to VariantProps. The slot-mode overload returns
(props?) => { [K in keyof S]: SlotFn }, so keyof ReturnType<T> is exactly
the set of slot names a component declares in tv({ slots: … }).
Use it to type a component's slotClasses prop from the single source of
truth (its *.variants.ts) instead of hand-maintaining a parallel union
that silently drifts when a slot is added or renamed: | |
VariantProps | type | helper | 1 | — | |
BuiltinMintName | type | helper | 0 | Built-in mint names as a literal union, so the mint prop autocompletes
across every component — the single list the hand-curated playground knobs
and docs used to drift away from. |
06 Installation
Import
import { FileUpload, IMAGE_MIME_TYPES, PDF_MIME_TYPE } from '@urbicon-ui/blocks';
import type {
FileUploadProps,
FileUploadFile,
FileRejection,
FileUploadError,
FileItemContext
} from '@urbicon-ui/blocks';