FileUpload
A file selection field backed by a real native input, single or multiple, with accept, maxSize, and maxFiles validation, a removable file list, and an optional drag-and-drop dropzone. It selects files rather than uploading them: the named input carries the chosen files into a plain form submission or into your own upload code.
Import
import { FileUpload } from "@hyzer-labs/ui"Demo
Single-file mode: an accepted pick replaces the current selection.
let file = $state<File[]>([]);
<FileUpload name="resume" label="Resume" bind:files={file} />multiple appends each accepted pick to the existing selection, de-duplicated
by name, size, and modified date. The picks render as a removable list with readable file
sizes.
let files = $state<File[]>([]);
<FileUpload name="photos" label="Photos" multiple bind:files={files} />The dropzone prop swaps the visible input for a drag-and-drop surface. The
native input is still what gets submitted: dropped files reach it through a rebuilt DataTransfer. The real Browse files button is always there as the
alternative to dragging.
Drag and drop files here, or
let files = $state<File[]>([]);
<FileUpload name="attachments" label="Attachments" dropzone multiple bind:files={files} />accept, maxSize, and maxFiles filter files and
report what they rejected. They do not set the field error themselves. This
demo maps onreject into the error string, then clears it on the
next accepted change.
let files = $state<File[]>([]);
let error = $state('');
function onreject(rejections: FileRejection[]) {
error = rejections.map((r) => r.message).join(' ');
}
<FileUpload
name="images"
label="Images (max 3, 1 MB each)"
multiple
accept="image/*"
maxSize={1_000_000}
maxFiles={3}
bind:files={files}
{error}
{onreject}
onchange={() => (error = "")}
/>The real named input carries the selected files into a plain form submission: new FormData(form).getAll(name) returns exactly the selected files, in order.
import { Form, FileUpload, Button, Stack } from '@hyzer-labs/ui';
<Form onSubmit={handleSubmit} ariaLabel="Attachments upload">
<Stack gap="md">
<FileUpload name="attachments-demo" label="Attachments" multiple bind:files={files} />
<div><Button type="submit">Submit</Button></div>
</Stack>
</Form>
// handleSubmit reads: new FormData(form).getAll("attachments-demo")description and error chain into aria-describedby. required sets aria-required and the visual asterisk, so
enforcing it is your code's job. Form renders the summary from the errors
you hand it, and validates nothing itself. Neither mode applies the native required attribute: a visually hidden required input in dropzone mode would be unfocusable, so both
stamp aria-required instead. disabled applies native disabled to the input and the activation button, and drops the remove buttons
from the list.
Description
Basic
PDF preferred; images are also accepted.
Dropzone
Drag and drop files here, or
PDF preferred; images are also accepted.
Error
Basic
A file is required.
Dropzone
Drag and drop files here, or
A file is required.
Required
Basic
Dropzone
Drag and drop files here, or
Disabled
Basic
Dropzone
Drag and drop files here, or
<!-- basic -->
<FileUpload
name="cert"
label="Certificate"
description="PDF preferred; images are also accepted."
/>
<FileUpload name="cert-error" label="Certificate" error="A file is required." />
<FileUpload name="cert-required" label="Certificate" required />
<FileUpload name="cert-disabled" label="Certificate" disabled />
<!-- dropzone: same states, side by side -->
<FileUpload
name="cert-dz"
label="Certificate"
dropzone
description="PDF preferred; images are also accepted."
/>
<FileUpload name="cert-error-dz" label="Certificate" dropzone error="A file is required." />
<FileUpload name="cert-required-dz" label="Certificate" dropzone required />
<FileUpload name="cert-disabled-dz" label="Certificate" dropzone disabled />Props
| Name | Type | Default | Note |
|---|---|---|---|
name | string | — | Required. Carried by the native input. |
label | string | — | Required. Always in the DOM; sr-only with hideLabel. |
files | File[] | [] | Bindable. Selected files in selection order — not a FileList. |
multiple | boolean | false | Single mode replaces on each pick; multiple mode appends + de-dupes. |
accept | string | — | Native accept syntax: extensions, exact MIME types, or wildcard MIME types. |
maxSize | number | — | Bytes, per file. No total-size cap. |
maxFiles | number | — | Multiple mode only — ignored in single mode (the cap is always 1). |
dropzone | boolean | false | Swaps the visible input for a drag-and-drop surface + activation button. |
buttonText | string | 'Browse files' | Dropzone mode only. |
dropzoneText | string | 'Drag and drop files here, or' | Dropzone mode only. |
onchange | (files: File[]) => void | — | Fires on every accepted change: add, remove, or single-mode replace. |
onreject | (rejections: FileRejection[]) => void | — | Fires whenever a pick or drop yields rejected files — see FileRejection below. |
description | string | — | Help text below the label. |
error | string | — | Inline error message; sets the error state. The component never sets this itself. |
required | boolean | false | |
disabled | boolean | false | |
hideLabel | boolean | false | |
class | string | — | Merged after the hz-field hz-file-upload classes. |
FileRejection
| Name | Type | Default | Note |
|---|---|---|---|
file | File | — | The rejected file. |
reason | 'type' | 'size' | 'too-many' | — | Accept mismatch, over maxSize, or beyond the count cap. |
message | string | — | A ready-to-display English string. |
Theme hooks
What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.
Root class: .hz-field hz-file-upload
Data attributes
| Hook | Values | Styles |
|---|---|---|
data-state | 'default' | 'error' | 'disabled' | The universal field state hook, on the root. Precedence is fixed: error beats disabled. |
data-dropzone | present in dropzone mode | On the root. The reference theme discriminates the two modes by targeting the parts instead, so this one is yours. |
data-dragover | present while dragging over | On .hz-file-dropzone. Backed by a depth counter, so it survives dragging across child elements. |
Part classes
| Hook | Values | Styles |
|---|---|---|
.hz-field-label | child element | The label. |
.hz-field-required | child element | The required marker. |
.hz-field-description | child element | The hint text. |
.hz-field-error | child element | The error message. |
.hz-file-control | child element | The box in basic mode. |
.hz-file-dropzone | child element | The box in dropzone mode. |
.hz-file-dropzone-text | child element | The dropzone prompt. |
.hz-file-button | child element | The browse button. |
.hz-file-list | child element | The selected-file list. |
.hz-file-item | child element | One file row. |
.hz-file-name | child element | The file name. |
.hz-file-size | child element | The formatted size. |
.hz-file-remove | child element | The per-file remove button. Also load-bearing: focus placement after a removal queries it. |
Accessibility
Drag-and-drop is a progressive enhancement, so the non-drag alternative is always present. In basic mode, the visible native input opens the platform picker directly. In dropzone mode, a real <button> (buttonText) opens the same picker. No function ever depends on a dragging movement (WCAG 2.5.7).
The native input is labeled by the field's <label for>. In dropzone mode the input is aria-hidden and out of the tab order, because the button is the control you operate. description and error chain into aria-describedby on the input, description first. required sets aria-required, and an error sets aria-invalid.
Each selected file's remove button carries its own Remove label naming that file. Additions, removals, and rejection counts are announced through a polite aria-live status region. That region sits outside the file list, so screen reader users hear changes the re-rendered list alone would not surface.
The theme's dragover state changes the background and the border, not the color alone.
References: MDN: <input type="file">