HyFileUpload
Usage examples on this page are written for Lit / plain HTML (
<hy-file-upload>). The same component ships asHyFileUploadin@whitespaceux/harmony-react(native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.
A file upload component with drag-and-drop support, client-side validation, image previews, and full keyboard + screen reader accessibility.
The component does NOT ship a network layer. Consumers wire their own upload transport via the add / change / remove events and the setProgress() / setError() public methods.
disabled suppresses ALL clicks from slotted content — use readonly to gate a bare wrapper. The element is form-associated, so when disabled is set the browser treats it like a native disabled form control and confines trusted click events to its subtree: clicks on anything slotted inside (buttons, links) never propagate past the host, and document-delegated handlers — including every Svelte 5 onclick — silently never fire. Keyboard and input events are NOT suppressed, which masks the bug ("Enter works, clicking doesn't"). This is UA behavior and cannot be opted out of. When the component wraps interactive content (bare mode) and you only want to block file ingestion, set readonly instead — it gates open(), drag-drop, and keyboard activation without touching slotted clicks.
When to use
- Attaching files to a form or a record: documents, images, exports.
- A screen where people bring several files at once, with a drop zone.
Do
- Set
accept,max-sizeandmax-filesto what the product takes. - Leave
multipleoff when only one file is expected.
When not to use
- A link or an identifier is enough:
hy-text-input type="url"or a plainhy-text-input. - Images inside Markdown text:
hy-markdown-editorwith itsuploadImagehook, not a second uploader beside it.
Accessibility: your part
- Tell people the accepted types and the size limit before they pick a file.
- If you replace the
placeholderslot, keep a visible way to browse. Dragging must never be the only way in. - Show every
rejectin text, naming the file, for example inerror-message. A border color alone is not announced.
Examples
Basic
<hy-file-upload label="Upload" accept="image/*" multiple></hy-file-upload>With validation
<hy-file-upload
label="Documents"
accept=".pdf,.doc,.docx"
max-size="5242880"
max-files="5"
multiple
></hy-file-upload>Seeding the list, and a batch in flight
<hy-file-upload id="docs" label="Documents" multiple></hy-file-upload>
<script>
const el = document.getElementById('docs');
el.files = [new File(['…'], 'brief.pdf', { type: 'application/pdf' })];
el.loading = true; // while the batch uploads: no new files, a busy drop zone
</script>Error and warning
<hy-file-upload label="CV" error-message="Attach your CV to continue."></hy-file-upload>
<hy-file-upload
label="Photo"
warning-message="Large files take a while to upload."
></hy-file-upload>API
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
label | label | string | '' | Accessible label rendered above the drop zone. |
name | name | string | '' | Form control name — files are submitted under this key. |
accept | accept | string | '' | Comma-separated list of allowed MIME types / extensions (.pdf,image/*). |
maxSize | max-size | number | undefined | — | Maximum size per file, in bytes. |
maxFiles | max-files | number | undefined | — | Maximum number of files allowed in total. |
multiple | multiple | boolean | false | Allows selecting more than one file. |
disabled | disabled | boolean | false | Disables all interaction. Because the element is form-associated, the UA also suppresses trusted clicks from the entire slotted subtree — never use this on a bare wrapper around interactive content; use readonly there (see the class docs). |
readonly | readonly | boolean | false | Prevents adding/removing files but keeps the list visible. The trigger (drop zone or button) and every row's remove button stay in place — inert, out of the accessibility tree and painted quiet, never disabled — so the component keeps its shape. Unlike disabled, slotted content stays fully interactive — the right way to gate ingestion on a bare wrapper. |
loading | loading | boolean | false | Busy state while a batch is uploading. The drop zone shows a spinner and an "Uploading…" caption, announces aria-busy, and stops taking new files (click, Enter / Space, drag-drop and open() all do nothing); the button variant shows its loading state. Files already in the list keep their remove button, so a transfer can still be canceled. |
files | — | File[] | [] | The accepted files, in order. The component updates it as files are added and removed (and fires change); assign it to seed the list — a draft restored from storage, files already on the server wrapped as File objects — or to replace it. Assigning fires no event. Property only (no attribute). Not used in bare mode, which retains nothing. |
error | error | boolean | false | Error state: the files are not accepted and something has to change. Paints the drop zone border in the danger color and shows error-message. Setting error-message raises it; clearing it lowers it. |
warning | warning | boolean | false | Warning state: the files are accepted but worth a second look. Paints the drop zone border in the warning color and shows warning-message; never touches form validity. error wins when both are set. |
variant | variant | 'drop-zone' | 'button' | 'drop-zone' | Visual form: full-bleed drop-zone (default) or compact button. |
size | size | 'small' | 'medium' | 'large' | 'medium' | Size variant. |
directory | directory | boolean | false | Enables folder selection (sets webkitdirectory on the native input). |
helperText | helper-text | string | '' | Helper text below the drop zone. |
errorMessage | error-message | string | '' | Error message below the drop zone (replaces the helper text). Setting it raises error. |
warningMessage | warning-message | string | '' | Warning message below the drop zone (replaces the helper text). Setting it raises warning. |
bare | bare | boolean | false | Headless mode. Renders only the default <slot> wrapped as a drag-drop target plus a hidden file input — no drop-zone chrome, no built-in file list, no label / helper. Each picked or dropped file is still validated against accept / maxSize, emitting add / reject, but the component retains no files and participates in no form: the consumer owns all state and UI (cf. react-dropzone, Ant showUploadList={false}). Trigger the picker via the open() method. maxFiles is enforced per drop batch only. |
strings | — | Partial<FileUploadStrings> | {} | The built-in strings, merged over DEFAULT_FILE_UPLOAD_STRINGS: set only the keys you translate. Property only (no attribute). Use for internationalization. |
form | — | HTMLFormElement | null | — | — |
validity | — | ValidityState | — | — |
Events
| Event | Detail | Description |
|---|---|---|
change | — | Accepted files changed. Detail: |
input | — | Files were picked/dropped, before validation. Detail: |
add | — | A file passed validation. Detail: |
remove | — | A file was removed from the list. Detail: |
reject | — | A file was rejected by validation. Detail: |
Slots
| Slot | Description |
|---|---|
default | Default slot (bare mode only): content wrapped as the drag-drop target. |
placeholder | Replaces the default drop-zone empty state. |
file:<name> | Per-file details rendered inside that file's row, under its name / size / progress — e.g. <div slot="file:Acme-Bold.otf">. The consumer keeps its own state per file (what an uploaded face IS, an image's alt text…) and the component keeps owning the row: thumbnail, transfer caption, progress bar, error, remove. Files that share a name share the slot. React mirror: the renderFileDetails(file) render prop — a slot name that varies per file cannot become a fixed <name>Slot prop, so the divergence is frozen in scripts/api-parity-baseline.json. |
CSS Parts
| Part | Description |
|---|---|
base | The host wrapper. |
label | The label element. |
drop-zone | The drag target container. |
trigger | The "Browse" button / trigger. |
placeholder | The empty-state area. |
file-list | The container holding all rows. |
file-item | A single file row. |
file-thumbnail | Image preview / file-type icon. |
file-progress | Per-row upload progress bar. |
file-progress-row | Row wrapping the progress bar and its percent label. |
file-progress-label | Per-row percent label beside the progress bar. |
file-remove | Per-row remove button. |
helper-text | Helper text container. |
error-message | Error message container. |
warning-message | Warning message container. |
message-icon | The icon starting the error or warning message. |
CSS Custom Properties
The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.
| Property | Default | Description |
|---|---|---|
--hy-file-upload-background-surface-subtle | var(--hy-background-surface-subtle) | Drop zone background color. |
--hy-file-upload-background-brand-tint-subtle | var(--hy-background-brand-tint-subtle) | Background while dragging. |
--hy-file-upload-border-subtle-rest | var(--hy-border-subtle-rest) | Drop zone border color. |
--hy-file-upload-border-brand-rest | var(--hy-border-brand-rest) | Border color while dragging. |
--hy-file-upload-border-danger-rest | var(--hy-border-danger-rest) | Drop zone border color in the error state. |
--hy-file-upload-border-warning-rest | var(--hy-border-warning-rest) | Drop zone border color in the warning state. |
--hy-file-upload-foreground-warning-rest | var(--hy-foreground-warning-rest) | Warning message text and icon color. |
--hy-file-upload-gap-layout-2xs | var(--hy-gap-layout-2xs) | Gap between the message icon and its text. |
--hy-file-upload-stroke-sm | var(--hy-stroke-sm) | Border width. |
--hy-file-upload-radius-form-md | var(--hy-radius-form-md) | Drop zone border radius. |
--hy-file-upload-padding-layout-xl | var(--hy-padding-layout-xl) | Vertical padding inside drop zone. |
--hy-file-upload-padding-layout-xl | var(--hy-padding-layout-xl) | Horizontal padding inside drop zone. |
--hy-file-upload-gap-form-lg | var(--hy-gap-form-lg) | Gap between file rows. |
--hy-file-upload-padding-layout-md | var(--hy-padding-layout-md) | Padding inside a file row. |
--hy-file-upload-radius-container-sm | var(--hy-radius-container-sm) | File row border radius. |
--hy-file-upload-placeholder-icon-size | — | Size of the empty-state icon (font-size on hy-icon). Default 2rem. |
--hy-file-upload-icon-size-md | var(--hy-icon-size-md) | Size of the per-file thumbnail icon. |
--hy-file-upload-motion-transition-hover-duration | var(--hy-motion-transition-hover-duration) | Transition duration for hover and focus state feedback |
--hy-file-upload-motion-transition-hover-easing | var(--hy-motion-transition-hover-easing) | Transition easing for hover and focus state feedback |
--hy-file-upload-motion-transition-expand-duration | var(--hy-motion-transition-expand-duration) | Transition duration for the file-list expand and collapse animation |
--hy-file-upload-motion-transition-expand-easing | var(--hy-motion-transition-expand-easing) | Transition easing for the file-list expand and collapse animation |
Methods
open()
Programmatically opens the native file picker.
clear()
Removes all files.
removeFile()
Removes a specific file from the list.
setProgress()
Updates per-file progress (0–100). Consumers call this from their upload transport. While progress sits below 100 the row shows an uploading caption (transferred of total) and a percent label beside the bar; at 100 the row settles back to the plain final size.
setError()
Marks a file row with an error message.
clearError()
Clears a previously-set error on a specific file.
focus()
Focuses the control: the drop zone, or the trigger button in the button variant. A bare upload has no control of its own (its slotted content is the consumer's), so there is nothing to focus.