icon

Morainev0.5.0

FileUpload
formsfile-upload

FileUpload

Drag-and-drop file upload component with progress tracking and file list management.

Import#

import { FileUpload } from 'moraine'

Slot Structure#

Upload control and optional file preview list.

Upload area#

root
└── control (dropzone or trigger button)
└── wrapper
├── icon (Icon)
├── label (optional)
└── description (optional)

File preview list#

root
└── files
└── file (×n)
├── filePreview
├── fileMeta
│ ├── fileName
│ └── fileSize
└── fileRemove

Examples#

Single Upload#

Basic single-file mode with a live readout from onValueChange.

function SingleUpload() {
type FileUploadValue = FileUploadT.Value
function fileNames(value: FileUploadValue): string {
if (value === null) {
return 'none'
}
if (Array.isArray(value)) {
return value.length > 0 ? value.map((file) => file.name).join(', ') : 'none'
}
return value.name
}
const [singleValue, setSingleValue] = createSignal<FileUploadValue>(null)
return (
<div class="max-w-xl space-y-3">
<FileUpload
label="Upload one file"
description="PNG, JPG, PDF up to your browser limit."
accept="image/*,.pdf"
onValueChange={setSingleValue}
/>
<p class="text-xs text-muted-foreground">Selected file: {fileNames(singleValue())}</p>
</div>
)
}

Multiple + Max Files#

Append files across selections, reject overflow, and show selected names.

function MultipleMaxFiles() {
function fileCount(value: FileUploadValue): number {
if (value === null) {
return 0
}
if (Array.isArray(value)) {
return value.length
}
return 1
}
function fileNames(value: FileUploadValue): string {
if (value === null) {
return 'none'
}
if (Array.isArray(value)) {
return value.length > 0 ? value.map((file) => file.name).join(', ') : 'none'
}
return value.name
}
const [multiValue, setMultiValue] = createSignal<FileUploadValue>([])
const [rejectedCount, setRejectedCount] = createSignal(0)
type FileUploadValue = FileUploadT.Value
return (
<div class="max-w-xl space-y-3">
<FileUpload
multiple
maxFiles={3}
accept="image/*,.pdf"
label="Upload up to 3 files"
description="Drop or select multiple files."
onValueChange={setMultiValue}
onFileReject={(files) => setRejectedCount(files.length)}
/>
<p class="text-xs text-muted-foreground">Selected count: {fileCount(multiValue())}</p>
<p class="text-xs text-muted-foreground">Selected names: {fileNames(multiValue())}</p>
<p class="text-xs text-muted-foreground">Last reject batch size: {rejectedCount()}</p>
</div>
)
}

Sizes#

Size scale from xs to xl for trigger height, spacing, and file list density.

function Sizes() {
const SIZES: FileUploadSize[] = ['xs', 'sm', 'md', 'lg', 'xl']
return (
<div class="max-w-xl space-y-3">
<For each={SIZES}>
{(size) => (
<FileUpload
size={size}
dropzone={false}
preview={false}
label={`Upload (${size})`}
description="Click to choose files."
/>
)}
</For>
</div>
)
}

Trigger Mode (No Dropzone)#

Use button-style trigger behavior by disabling dropzone interaction.

function TriggerModeNoDropzone() {
return (
<div class="max-w-xl">
<FileUpload
dropzone={false}
label="Select file"
description="Click to choose files."
preview={false}
/>
</div>
)
}

Form Integration#

Submit to validate a required attachment with Form + FormField.

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 of={form}>
<div class="max-w-xl space-y-4">
<FormField
name="attachment"
label="Attachment"
description="Upload at least one file before submit."
required
>
<FileUpload id="demo-attachment-upload" onValueChange={updateFormAttachment} />
</FormField>
<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>
)
}

API Reference#

Attributes#

Slotroot3 attributes
Upload component container that owns dropzone, file input, and file list.

Data Attributes

2
Data AttributeDescription
data-disabled
Present when the component or item is disabled.
data-readonly
Present when the field is read-only.

ARIA Attributes

1
ARIA AttributeDescription
role
Defines the semantic role exposed to assistive technology.

Props#

PropTypeDefaultDescription
acceptstring | undefined*
Accepted file types (e.g., ".jpg,.png", "image/*").
as"div" | undefineddiv
The HTML element or component to render as.
classClassValue
Class applied to the component root or trigger element.
classesFileUploadT.Classes | undefined
descriptionJSX.Element
Description text for the upload area.
disabledboolean | undefinedfalse
Whether the input is disabled.
dropzoneboolean | undefinedtrue
Whether to enable drag and drop.
fileIconIconT.Nameicon-file
Icon to show for individual files when no preview is available.
iconIconT.Nameicon-upload
Icon to show in the upload area.
idstring | undefined
The ID of the input element.
labelJSX.Element
Label for the upload area.
maxFilesnumber | undefined
Maximum number of files allowed.
maxSizenumber | undefined
Maximum accepted file size in bytes.
minSizenumber | undefined
Minimum accepted file size in bytes.
multipleboolean | undefinedfalse
Whether multiple files can be uploaded.
namestring | undefined
The name of the input element, used for form submission.
onClickJSX.EventHandlerUnion<HTMLElement, MouseEvent> | undefined
Click handler for the upload control.
onDragLeaveJSX.EventHandlerUnion<HTMLElement, DragEvent> | undefined
Drag-leave handler for the upload dropzone.
onDragOverJSX.EventHandlerUnion<HTMLElement, DragEvent> | undefined
Drag-over handler for the upload dropzone.
onDropJSX.EventHandlerUnion<HTMLElement, DragEvent> | undefined
Drop handler for the upload dropzone.
onFileReject((files: FileRejection[]) => void) | undefined
Callback when files are rejected (e.g., due to type or count).
onKeyDownJSX.EventHandlerUnion<HTMLElement, KeyboardEvent> | undefined
Keyboard handler for the upload control.
onValueChange((value: FileUploadT.Value) => void) | undefined
Callback when the selected files change.
previewboolean | undefinedtrue
Whether to show file previews.
readOnlyboolean | undefinedfalse
Whether the input is read-only.
refJSX.HTMLElementTags["div"] extends { ref?: infer Ref; } ? Ref : never | undefined
requiredboolean | undefinedfalse
Whether the input is required.
size"xs" | "sm" | "md" | "lg" | "xl" | undefined
styleJSX.CSSProperties | undefined
stylesFileUploadT.Styles | undefined