Skip to main content

File Upload

File Upload is a form control that lets people pick or drop files, such as attaching documents or uploading photos.

Import

import { FileUpload } from '@vortexlabs/vortex';
import type { FileUploadProps } from '@vortexlabs/vortex';

Other links

Attachments
Drag and drop files hereSVG, PNG, JPG or GIF, up to 10 MB
<FileUpload.Root>
  <FileUpload.Label>Attachments</FileUpload.Label>
<FileUpload.Dropzone name="attachments" multiple description="SVG, PNG, JPG or GIF, up to 10 MB" />
<FileUpload.ErrorMessage>Upload at least one file.</FileUpload.ErrorMessage>
</FileUpload.Root>

Features

  • Accepts files from the file picker or drag and drop.
  • Passes selected files to onChange; your app uploads them.
  • Supports single or multiple files and a file type filter for the picker.
  • Lists files as in progress, completed, or failed.
  • Supports cancel, remove, and retry actions on each file.
  • Supports custom dropzone titles, icon, button, and description.

Anatomy

<FileUpload.Root>
  <FileUpload.Label>Attachments</FileUpload.Label>
  <FileUpload.Dropzone name="attachments" />
  <FileUpload.ErrorMessage>Upload at least one file.</FileUpload.ErrorMessage>
  <FileUpload.List>
    <FileUpload.Item fileName="report.pdf" />
    <FileUpload.Item fileName="photo.png" />
  </FileUpload.List>
</FileUpload.Root>
Component Part
Description
Links the label and messages to the file input, and shares the disabled and error states.
Names the file input.
Receives dropped files and opens the file picker from its button.
Explains the error while the root error state is set.
Groups the file items and renders nothing while empty.
Shows one file's name, details, progress, and actions.

Dropzone Content

Customize the dropzone with title, dragOverTitle, buttonLabel, buttonProps, and description. Set accept to filter the file picker, and startSlot={null} to hide the upload icon.

Drop your avatar herePNG or JPG, up to 2 MB
Drag and drop files here
import { FileUpload, Stack } from "@vortexlabs/vortex";

<Stack gap={6}>
  <FileUpload.Root>
    <FileUpload.Dropzone
      name="avatar"
      title="Drop your avatar here"
      dragOverTitle="Release to upload"
      buttonLabel="Browse"
      buttonProps={{ variant: "primary" }}
      description="PNG or JPG, up to 2 MB"
      accept="image/png,image/jpeg"
    />
  </FileUpload.Root>
  <FileUpload.Root>
    <FileUpload.Dropzone name="minimal" startSlot={null} />
  </FileUpload.Root>
</Stack>

File Items

Set status to in-progress (shows progress), completed, or error. Actions appear only with their callbacks: onCancel while uploading, onRemove when completed or failed, and onRetry for retryable errors.

  • Brand guidelines.pdf2.5 MB
    50%
  • Quarterly report.pdf1.8 MB · Uploaded
  • Team photo.pngUpload failed. Check your connection.
  • presentation.keyUnsupported file format.
<FileUpload.List>
  <FileUpload.Item
    fileName="Brand guidelines.pdf"
    status="in-progress"
    description="2.5 MB"
    progress={50}
    onCancel={handleCancel}
  />
  <FileUpload.Item
    fileName="Quarterly report.pdf"
    description="1.8 MB"
    statusText="Uploaded"
    onRemove={handleRemove}
  />
  <FileUpload.Item
    fileName="Team photo.png"
    status="error"
    errorMessage="Upload failed. Check your connection."
    onRetry={handleRetry}
    onRemove={handleRemove}
  />
  <FileUpload.Item
    fileName="presentation.key"
    status="error"
    errorMessage="Unsupported file format."
    onRemove={handleRemove}
  />
</FileUpload.List>

Disabled and Error States

Set error on FileUpload.Root to show FileUpload.ErrorMessage. Set disabled to block picking and dropping files; item actions stay available.

Attachments
Drag and drop files hereSVG, PNG, JPG or GIF, up to 10 MB
Upload at least one file.
Attachments
Drag and drop files hereSVG, PNG, JPG or GIF, up to 10 MB
<Stack gap={6}>
  <FileUpload.Root error>
    <FileUpload.Label>Attachments</FileUpload.Label>
    <FileUpload.Dropzone name="error-files" description="SVG, PNG, JPG or GIF, up to 10 MB" />
    <FileUpload.ErrorMessage>Upload at least one file.</FileUpload.ErrorMessage>
  </FileUpload.Root>
  <FileUpload.Root disabled>
    <FileUpload.Label>Attachments</FileUpload.Label>
    <FileUpload.Dropzone name="disabled-files" description="SVG, PNG, JPG or GIF, up to 10 MB" />
  </FileUpload.Root>
</Stack>

Uploading Files

onChange receives the picked or dropped files. File Upload doesn't upload them or submit them with a native form. Without multiple, only the first dropped file is passed. Validate each file, upload it, and render a FileUpload.Item with its current status.

Photos
Drag and drop files herePNG or JPG, up to 10 MB
"use client";

import { FileUpload } from "@vortexlabs/vortex";
import React from "react";

type Upload = {
  id: string;
  file: File;
  progress: number;
  status: "in-progress" | "completed" | "error";
  errorMessage?: string;
};

const ACCEPTED_TYPES = ["image/png", "image/jpeg"];
const MAX_SIZE = 10 * 1024 * 1024;

const formatSize = (bytes: number) => {
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
};

export const FileUploadLifecycleExample = () => {
  const [uploads, setUploads] = React.useState<Upload[]>([]);
  const timersRef = React.useRef(new Map<string, ReturnType<typeof setInterval>>());

  React.useEffect(() => {
    const timers = timersRef.current;
    return () => {
      for (const timer of timers.values()) clearInterval(timer);
    };
  }, []);

  const update = (id: string, patch: Partial<Upload>) => {
    setUploads((prev) => prev.map((upload) => (upload.id === id ? { ...upload, ...patch } : upload)));
  };

  // Simulated progress; replace with your upload request
  const startUpload = (id: string) => {
    let progress = 0;
    const timer = setInterval(() => {
      progress = Math.min(progress + 10, 100);
      if (progress < 100) {
        update(id, { progress });
        return;
      }
      clearInterval(timer);
      timersRef.current.delete(id);
      update(id, { progress, status: "completed" });
    }, 200);
    timersRef.current.set(id, timer);
  };

  const remove = (id: string) => {
    clearInterval(timersRef.current.get(id));
    timersRef.current.delete(id);
    setUploads((prev) => prev.filter((upload) => upload.id !== id));
  };

  const handleChange = ({ value }: { value: File[] }) => {
    const next = value.map<Upload>((file) => {
      const id = crypto.randomUUID();
      if (!ACCEPTED_TYPES.includes(file.type)) {
        return { id, file, progress: 0, status: "error", errorMessage: "Unsupported file format." };
      }
      if (file.size > MAX_SIZE) {
        return { id, file, progress: 0, status: "error", errorMessage: "File is larger than 10 MB." };
      }
      return { id, file, progress: 0, status: "in-progress" };
    });

    setUploads((prev) => [...prev, ...next]);
    for (const upload of next) {
      if (upload.status === "in-progress") startUpload(upload.id);
    }
  };

  return (
    <FileUpload.Root>
      <FileUpload.Label>Photos</FileUpload.Label>
      <FileUpload.Dropzone
        name="photos"
        multiple
        accept={ACCEPTED_TYPES.join(",")}
        description="PNG or JPG, up to 10 MB"
        onChange={handleChange}
      />
      <FileUpload.List>
        {uploads.map((upload) => (
          <FileUpload.Item
            key={upload.id}
            fileName={upload.file.name}
            status={upload.status}
            description={formatSize(upload.file.size)}
            statusText="Uploaded"
            progress={upload.progress}
            errorMessage={upload.errorMessage}
            onCancel={() => remove(upload.id)}
            onRemove={() => remove(upload.id)}
          />
        ))}
      </FileUpload.List>
    </FileUpload.Root>
  );
};

Accessibility

Key
Description
TabShiftTab
Move focus to the file input and each file's actions.
EnterSpace
Open the file picker, or run the focused action.
Include FileUpload.Label; without it, the input is named only by its button.
Upload status isn't announced; announce finished and failed uploads yourself.
Translate cancelAriaLabel, removeAriaLabel, and retryLabel; the defaults are English.
Validate files in your app; required doesn't check them.