---
title: FileUpload
description: Select and validate local files through a dropzone or trigger.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# FileUpload

> Select and validate local files through a dropzone or trigger.

Use FileUpload to collect local files before the application sends them. Selection and validation happen in the component; upload transport belongs to the caller.

## Basic usage

```tsx
import { FileUpload } from 'moraine'

export function Example() {
  return <FileUpload label="Upload files" />
}
```

## Anatomy

```text
FileUpload [component; slot=root]
├── control [slot]
│   └── wrapper [slot]
│       ├── icon [slot]
│       ├── label [slot]
│       └── description [slot]
├── input [internal]
└── files [slot]
    └── file [slot]
        ├── filePreview [slot]
        ├── fileMeta [slot]
        │   ├── fileName [slot]
        │   └── fileSize [slot]
        └── fileRemove [slot]
```

## Usage

### Selecting files

Click anywhere in the visible dropzone, including its padding, icon, and text, or drop files onto it. File previews and removal actions are outside the picker hit area. Use the dropzone for pointer and drag-and-drop selection, or set `dropzone={false}` for a compact picker trigger. Use `multiple` when the value can contain more than one `File`; `onValueChange` receives either a single file, an array, or `null`.

```tsx
import { FileUpload } from 'moraine'
import { For } from 'solid-js'

export function SelectingFiles() {
  return (
    <div class="flex flex-col gap-6 max-w-md w-full">
      <For each={['sm', 'md', 'lg'] as const}>
        {(size) => (
          <div class="flex flex-col gap-2">
            <span class="text-xs text-muted-foreground font-medium">{size}</span>
            <FileUpload
              size={size}
              multiple
              label="Click anywhere or drop files"
              description="Choose documents and images."
            />
          </div>
        )}
      </For>
    </div>
  )
}
```

### Rejections and form behavior

`accept`, `minSize`, `maxSize`, and `maxFiles` validate an attempted selection before it is added. Handle `onFileReject` to present rejected files and their reason codes in your application. `readOnly` prevents changes while preserving the selected list. It inherits an enclosing Field read-only state when omitted; an explicit `readOnly={false}` overrides that state. `disabled` also removes interaction.

```tsx
import { FileUpload } from 'moraine'

export function FormBehavior() {
  return (
    <div class="max-w-md w-full space-y-4">
      <FileUpload
        disabled
        label="Disabled upload"
        description="Upload is locked during maintenance."
      />
      <FileUpload
        readOnly
        label="Read-only upload"
        description="View existing attachments without making modifications."
      />
    </div>
  )
}
```

### Keyboard interaction

| Key                                 | Description                                                                              |
| ----------------------------------- | ---------------------------------------------------------------------------------------- |
| <kbd>Enter</kbd> / <kbd>Space</kbd> | Opens the native operating system file picker dialog when the upload control is focused. |
| <kbd>Tab</kbd>                      | Navigates between the upload dropzone control and individual file removal buttons.       |

## Examples

### Multiple files

```tsx
import { Badge, FileUpload } from 'moraine'
import type { FileUploadT } from 'moraine'
import { createSignal, For, Show } from 'solid-js'

export function MultipleMaxFiles() {
  type FileUploadValue = FileUploadT.Value

  const [receipts, setReceipts] = createSignal<FileUploadValue>([])
  const [rejectWarning, setRejectWarning] = createSignal<string | null>(null)

  const filesList = () => {
    const val = receipts()
    if (!val) {
      return []
    }
    return Array.isArray(val) ? val : [val]
  }

  return (
    <div class="p-4 b-1 b-border rounded-xl max-w-xl space-y-4">
      <div class="flex items-center justify-between">
        <div>
          <h4 class="text-sm font-medium">Expense receipts</h4>
          <p class="text-xs text-muted-foreground">
            Upload up to 3 receipt images or PDF documents.
          </p>
        </div>
        <Badge variant="outline">Max 3 files</Badge>
      </div>

      <FileUpload
        multiple
        maxFiles={3}
        accept="image/*,.pdf"
        label="Drop receipts here"
        description="PDF, PNG, JPG up to 10MB each"
        onValueChange={(val) => {
          setReceipts(val)
          setRejectWarning(null)
        }}
        onFileReject={(rejected) => {
          setRejectWarning(
            `Rejected ${rejected.length} file(s) exceeding the 3 file limit or invalid format.`,
          )
        }}
      />

      <Show when={rejectWarning()}>
        <p class="text-xs text-destructive">{rejectWarning()}</p>
      </Show>

      <Show when={filesList().length > 0}>
        <div class="pt-2 border-t border-border space-y-2">
          <p class="text-xs text-muted-foreground font-medium">
            Attached files ({filesList().length}/3):
          </p>
          <div class="space-y-1">
            <For each={filesList()}>
              {(file) => (
                <div class="text-xs p-2 rounded-lg bg-muted/40 flex items-center justify-between">
                  <span class="font-medium truncate">{file.name}</span>
                  <span class="text-muted-foreground font-mono">
                    {(file.size / 1024).toFixed(1)} KB
                  </span>
                </div>
              )}
            </For>
          </div>
        </div>
      </Show>
    </div>
  )
}
```

### File rejections

```tsx
import { FileUpload } from 'moraine'
import { For, createSignal } from 'solid-js'

const REJECTION_LABELS = {
  FILE_DUPLICATE: 'already selected',
  FILE_INVALID_TYPE: 'type is not accepted',
  FILE_TOO_LARGE: 'is larger than 1 MB',
  FILE_TOO_MANY_FILES: 'exceeds the file limit',
  FILE_TOO_SMALL: 'is smaller than the minimum size',
  TOO_MANY_FILES: 'exceeds the file limit',
} as const

export function Rejections() {
  const [rejections, setRejections] = createSignal<{ name: string; reason: string }[]>([])

  return (
    <div class="max-w-lg space-y-3">
      <FileUpload
        multiple
        accept="image/png,image/jpeg"
        maxFiles={2}
        maxSize={1024 * 1024}
        label="Upload images"
        description="PNG or JPEG, up to 1 MB each (maximum two files)"
        onFileReject={(files) =>
          setRejections(
            files.map(({ file, errors }) => ({
              name: file.name,
              reason: errors.map((error) => REJECTION_LABELS[error]).join(', '),
            })),
          )
        }
      />

      <For each={rejections()}>
        {(rejection) => (
          <p class="text-sm text-destructive">
            {rejection.name}: {rejection.reason}
          </p>
        )}
      </For>
    </div>
  )
}
```

### Trigger-only picker

```tsx
import { FileUpload } from 'moraine'
import type { FileUploadT } from 'moraine'
import { createSignal, Show } from 'solid-js'

export function TriggerModeNoDropzone() {
  const [attached, setAttached] = createSignal<FileUploadT.Value>(null)

  const fileName = () => {
    const file = attached()
    if (!file) {
      return null
    }
    return Array.isArray(file) ? file[0]?.name : file.name
  }

  return (
    <div class="max-w-md space-y-3">
      <FileUpload
        dropzone={false}
        label="Attach files"
        description="Choose receipts or invoices from your computer."
        onValueChange={setAttached}
      />

      <Show when={fileName()}>
        <div class="text-xs text-muted-foreground px-3 py-2 rounded-lg bg-muted/40 flex gap-2 items-center">
          <span class="i-lucide:paperclip text-primary" />
          <span>Selected: {fileName()}</span>
        </div>
      </Show>
    </div>
  )
}
```

### Form integration

```tsx
import { Button, createForm, FileUpload } from 'moraine'
import type { FileUploadT } from 'moraine'
import { createSignal } from 'solid-js'
import * as v from 'valibot'

export function FormIntegration() {
  const [formState, setFormState] = createSignal({
    attachment: null as File | null,
  })

  const updateFormAttachment = (value: FileUploadValue) => {
    const next = Array.isArray(value) ? (value[0] ?? null) : value
    setFormState((prev) => ({ ...prev, attachment: next }))
  }

  type FileUploadValue = FileUploadT.Value
  const form = createForm({
    schema: v.object({ attachment: v.file('Please upload one attachment.') }),
    initialInput: { attachment: undefined },
  })

  return (
    <form.Form>
      <div class="max-w-xl space-y-4">
        <form.Field
          name="attachment"
          label="Attachment"
          description="Upload at least one file before submit."
          required
        >
          <FileUpload id="demo-attachment-upload" onValueChange={updateFormAttachment} />
        </form.Field>
        <div class="flex gap-3 items-center">
          <Button type="submit" variant="secondary" size="sm">
            Validate
          </Button>
          <p class="text-xs text-muted-foreground">
            Current attachment: {formState().attachment?.name ?? 'none'}
          </p>
        </div>
      </div>
    </form.Form>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-disabled` | `file-upload` | Present when the component, slot, or item is disabled. |
| `data-invalid` | `file-upload`, `file-upload-control` | Present when the field or form has a validation error. |
| `data-readonly` | `file-upload` | Present when the field is in read-only mode. |
| `data-required` | `file-upload` | Present when the field input is required. |
| `data-dropzone` | `file-upload-control`, `file-upload-wrapper` | Present when the target is an active file dropzone. |
| `data-dragging` | `file-upload-control` | Present while the related thumb or handle is being dragged. |

## Props

Props for the FileUpload component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| accept | string \| undefined | '*' | Accepted file types (e.g., ".jpg,.png", "image/*"). |
| defaultValue | Value<Multiple> \| undefined | — | The default value of the input (uncontrolled). |
| description | JSX.Element \| undefined | — | Description text for the upload area. |
| disabled | boolean \| undefined | false | Whether the input is disabled. |
| dropzone | boolean \| undefined | true | Whether to enable drag and drop. |
| fileIcon | IconT.Name \| undefined | 'icon-file' | Icon to show for individual files when no preview is available. |
| icon | IconT.Name \| undefined | 'icon-upload' | Icon to show in the upload area. |
| id | string \| undefined | — | The ID of the input element. |
| inputRef | Ref<HTMLInputElement> \| undefined | — | Native input element ref. |
| label | JSX.Element \| undefined | — | Label for the upload area. |
| maxFiles | number \| undefined | — | Maximum number of files allowed. |
| maxSize | number \| undefined | — | Maximum accepted file size in bytes. |
| minSize | number \| undefined | — | Minimum accepted file size in bytes. |
| multiple | Multiple \| undefined | false | Whether multiple files can be uploaded. |
| name | string \| undefined | — | The name of the input element, used for form submission. |
| onFileReject | ((files: Rejection[]) => void) \| undefined | — | Callback when files are rejected (e.g., due to type or count). |
| onValueChange | ((value: Value<Multiple>) => void) \| undefined | — | Callback when the selected file(s) change. |
| preview | boolean \| undefined | true | Whether to show file previews. |
| readOnly | boolean \| undefined | false | Whether the input is read-only. |
| required | boolean \| undefined | false | Whether the input is required. |
| size | 'sm' \| 'md' \| 'lg' \| undefined | 'md' | Visual size of the component. |
| value | Value<Multiple> \| undefined | — | The current value of the input (controlled). |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |
