Skip to main content
Urbicon UI

FileUpload

A drag-and-drop file picker that validates files, previews images, and lists them with a progress bar your upload code drives.

Playground

Drop files here, or click
Any file type, up to 10 MB
Size Style variant
Intent Style variant
<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. Each accepted file lists its name, size and a remove button.
Drop a file here, or click
Any file type, up to 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="Any file type, up to 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> file(s) selected
      </p>
    </div>
  {/if}
</div>

Images with a preview

FileUpload shows a thumbnail for each image. Below it, this example adds a grid of larger previews with overlaid filenames, to show pairing the component with a layout of your own.
Upload images
PNG, JPG, WebP, GIF or AVIF, up to 5 MB each
<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="Upload images"
    description="PNG, JPG, WebP, GIF or AVIF, up to 5 MB each"
  />

  <!-- 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

accept, maxFileSize and maxFiles are all enforced: PDF, DOCX or XLSX only, 2 MB each, three files at most. A rejected file shows up in an Alert with a per-file reason.
Upload documents
PDF, DOCX or XLSX, up to 2 MB, 3 files max
<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="Upload documents"
    description="PDF, DOCX or XLSX, up to 2 MB, 3 files max"
    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. Each file moves through pending, uploading, then complete or error; your upload code sets the status and percentage as it goes.
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: here a gradient background, an icon and a call to action, with slotClasses for the frame.

Upload design assets

Images and PDFs, up to 8 MB each

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">Upload design assets</p>
        <p class="text-text-tertiary mt-1 text-sm">Images and PDFs, up to 8 MB each</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, so native file selection and form submission keep working.

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