Components

FileUpload

Headless native file-picker, dropzone, validation, selected-file collection, and removal primitives.

When to Use

Use FileUpload when users must choose local files and the interface needs a custom picker, drop target, selected-file list, or client-side feedback. Use a plain input type="file" when the browser control is sufficient. Client checks improve feedback but are not a security boundary; validate every uploaded file again on the server.

Features

  • Supports controlled and uncontrolled file arrays.
  • Uses a hidden native file input for picker and form semantics.
  • Supports button and drag-and-drop selection.
  • Validates accept rules, count, size, and custom errors.
  • Appends or replaces multiple-file selections.
  • Exposes rejected files, drag state, item metadata, and removal actions.
  • Integrates IDs, descriptions, and state with Field.

Import

import { FileUpload } from "@flowstack-ui/atom";

Anatomy

<FileUpload.Root>
  <FileUpload.HiddenInput />
  <FileUpload.Trigger />
  <FileUpload.Dropzone />
  <FileUpload.ItemGroup>
    <FileUpload.Item>
      <FileUpload.ItemName />
      <FileUpload.ItemSize />
      <FileUpload.ItemDeleteTrigger />
    </FileUpload.Item>
  </FileUpload.ItemGroup>
</FileUpload.Root>

fileMatchesAccept()
validateFileUploadFiles()
formatFileSize()

API Reference

Root

Renders a div, owns selection and validation state, and supplies behavior to all parts. Local field-state props override Field context.

PropTypeDefault
filesFile[]-
defaultFilesFile[][]
onFilesChange(files: File[]) => void-
onRejectedFilesChange(files: FileUploadRejectedFile[]) => void-
acceptstring-
multiplebooleanfalse
appendFilesbooleantrue when multiple
maxFilesnumber1 when single; otherwise -
maxSizenumber-
validateFile(file) => string | null | undefined | false-
namestring-
formstring-
disabledbooleanField state or false
requiredbooleanField state or false
readOnlybooleanField state or false
invalidbooleanField state or false
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"file-upload"
[data-state]"filled" | "empty"
[data-drag]"idle" | "accept" | "reject"
[data-filled]Present with selected files
[data-rejected]Present with rejected files
[data-disabled]Present when disabled
[data-readonly]Present when read only
[data-required]Present when required
[data-invalid]Present when invalid

HiddenInput

Renders the hidden native input type="file", opens through Trigger, and sends selected FileList values into Root validation.

PropTypeDefault
onChangeChangeEventHandler<HTMLInputElement>-
ARIA/native attributeValues
idField control ID or generated Root control ID
name, form, accept, multipleRoot values
disabled, requiredResolved Root/Field state
aria-describedbyExplicit Root value or Field message IDs
aria-invalidResolved invalid state
Data attributeValues
[data-slot]"file-upload-hidden-input"

Trigger

Opens HiddenInput's native file picker. It renders a button by default and adds button semantics to non-native custom elements.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"button" for non-native custom elements
aria-disabled"true" when a custom trigger is disabled or read-only
Data attributeValues
[data-slot]"file-upload-trigger"
[data-disabled]Present when Root is disabled
[data-readonly]Present when Root is read-only

Dropzone

Renders a div that accepts dropped files. It owns drag events but adds no button role or tab stop; include Trigger when keyboard picker access is needed.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"file-upload-dropzone"
[data-drag]"idle" | "accept" | "reject"
[data-dragging]Present for accept/reject drag state
[data-accepted]Present for accepted drag state
[data-rejected]Present for rejected drag state
[data-disabled]Present when disabled
[data-readonly]Present when read-only

ItemGroup

Renders a ul for selected files. A function child receives each (file, index); function children cannot be combined with asChild.

PropTypeDefault
childrenReactNode | ((file, index) => ReactNode)-
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"file-upload-item-group"
[data-count]Selected file count
[data-filled]Present when count is greater than zero

Item

Renders one li and provides its resolved File and index to metadata/removal parts. It returns null when neither file nor the requested Root index exists.

PropTypeDefault
fileFileRoot file at index
indexnumber0
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"file-upload-item"
[data-index]Resolved file index
[data-name]File name
[data-size]File size in bytes

ItemName

Renders a span containing the File name by default. Consumer children replace that text; asChild and render are supported.

PropTypeDefault
childrenReactNodeCurrent file name
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"file-upload-item-name"

ItemSize

Renders a span containing formatFileSize(file.size) by default. Consumer children replace the formatted text; asChild and render are supported.

PropTypeDefault
childrenReactNodeFormatted current file size
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"file-upload-item-size"
[data-size]File size in bytes

ItemDeleteTrigger

Renders a button that removes the current Item, resets the native input so the same file can be chosen again, and respects disabled/read-only state.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-labelConsumer value or "Remove {file.name}"
Data attributeValues
[data-slot]"file-upload-item-delete-trigger"
[data-disabled]Present when disabled
[data-readonly]Present when read-only

fileMatchesAccept

Returns whether a File matches a comma-separated extension, exact MIME type, or wildcard MIME rule. No rule or an empty rule accepts the file.

validateFileUploadFiles

Returns { acceptedFiles, rejectedFiles }. Rejection errors contain "type", "size", "count", and any nonempty custom validation string.

formatFileSize

Formats bytes with binary-sized B, KB, MB, GB, or TB units and returns "0 B" for non-finite or nonpositive input.

The entry also exports useFileUploadContext and useFileUploadItemContext for advanced custom parts; both require their corresponding parent.

Examples

Multiple Documents

import { FileUpload } from "@flowstack-ui/atom";

export function DocumentUpload() {
  return (
    <FileUpload.Root name="documents" accept=".pdf,.txt" multiple maxFiles={5}>
      <FileUpload.HiddenInput />
      <FileUpload.Trigger>Choose documents</FileUpload.Trigger>
      <FileUpload.Dropzone>Drop PDF or text files here</FileUpload.Dropzone>
      <FileUpload.ItemGroup>
        {(file, index) => (
          <FileUpload.Item key={`${file.name}-${index}`} file={file}>
            <FileUpload.ItemName />
            <FileUpload.ItemSize />
            <FileUpload.ItemDeleteTrigger>Remove</FileUpload.ItemDeleteTrigger>
          </FileUpload.Item>
        )}
      </FileUpload.ItemGroup>
    </FileUpload.Root>
  );
}

Report Rejections

import { useState } from "react";
import { FileUpload, type FileUploadRejectedFile } from "@flowstack-ui/atom";

export function ImageUpload() {
  const [rejected, setRejected] = useState<FileUploadRejectedFile[]>([]);
  return (
    <FileUpload.Root
      accept="image/*"
      maxSize={2 * 1024 * 1024}
      onRejectedFilesChange={setRejected}
    >
      <FileUpload.HiddenInput />
      <FileUpload.Trigger>Choose image</FileUpload.Trigger>
      <p aria-live="polite">
        {rejected.length > 0 ? "The selected image was not accepted." : ""}
      </p>
    </FileUpload.Root>
  );
}

Accessibility

HiddenInput supplies native file-input semantics and must be present for Trigger to open a picker. Give the upload a visible Field.Label or another accessible name. Trigger and ItemDeleteTrigger use button keyboard behavior; Dropzone alone is pointer-only. Announce rejection feedback in visible text and validate file type, size, and content again on the server.

KeyDescription
Enter / SpaceOpens the picker from Trigger or removes from ItemDeleteTrigger.

Changelog

See CHANGELOG.md.