Skip to main content
Urbicon UI

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

Drop files here, or click
Beliebiger Dateityp, max. 10 MB
Size Style variant (tailwind-variants)
Intent Style variant (tailwind-variants)
<script lang="ts">
  import { FileUpload } from '@urbicon-ui/blocks';

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

<FileUpload
  bind:files
  intent="primary"
/>

01 Examples

Basic upload

One file, by drag-and-drop or click. The result renders as a file list with name, size and a remove button.
Drop a file here, or click
Beliebiger Dateityp, max. 10 MB
<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

Images become thumbnails on their own. Below the component sits a hand-built grid with overlaid filenames — FileUpload and a preview layout of your own, side by side.
Bilder hochladen
PNG, JPG, WebP, GIF, AVIF — max. 5 MB pro Bild
<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

File type (.pdf, .docx, .xlsx), a 2 MB size ceiling and a count of 3 are all checked. Rejections surface as an alert carrying structured messages.
Dokumente hochladen
Nur PDF, DOCX, XLSX — max. 2 MB, max. 3 Dateien
<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

A simulated upload with a bar per file. The indicators walk the lifecycle — pending → uploading → complete/error — and the consumer drives the progress from outside.
Choose files to upload
Klicke 'Upload starten' nach der Auswahl
<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

The 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

Choose files
<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

32 props
32 props
Prop
Type
Default
Description

05 Types

Local type definitions used by this component.

16 types
Name
Kind
Category
Used by
Description

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';