Dropzone
hc-dropzone decorates a native <input type="file"> with a
drop surface. The interactive element stays the input: the wrapping
<label> opens the picker on click, keyboard focus lands on the
(visually hidden, still focusable) input, and required / accept /
field-errors wiring attach to it as usual. Drag-and-drop is a
pointer-only enhancement — a drop assigns dataTransfer.files to the
input and fires a normal bubbling change, so everything downstream
(form serialization, the
file-upload recipe,
validation) cannot tell the file wasn’t browsed.
Basic HTML
Section titled “Basic HTML”<label class="hc-dropzone"> <input class="hc-dropzone__input" type="file" name="doc"> <span class="hc-dropzone__body"> <span class="hc-dropzone__hint">Drop a file here, or click to browse</span> <span class="hc-dropzone__files"></span> </span></label>.hc-dropzone__files shows the selected file names (set by the
behavior on every change, dropped or browsed); it collapses when
empty. The hint is authored markup — write it in your app’s language.
Behavior — installDropzone()
Section titled “Behavior — installDropzone()”Included in the auto-init
./behaviors
bundle. While a file drags over the zone it sets data-dragover
(styled below); a drop assigns the files to the input — a single-file
input takes only the first — and dispatches a bubbling change. Drags
that carry no files (text selections) are ignored, as are zones whose
input is disabled. No new events, no network.
States
Section titled “States”| State | Selector | Effect |
|---|---|---|
| Drag over | [data-dragover] | Solid accent border (--hc-dropzone-dragover-border, follows data-color) + tinted background. |
| Keyboard focus | :focus-within | Standard focus ring (focus lives on the input). |
| Disabled | :has(> .hc-dropzone__input:disabled) | Muted, cursor: not-allowed; the behavior ignores drops. |
| Invalid | :has(> .hc-dropzone__input[aria-invalid="true"]) | Error border — set by field-errors like any other control. |
data-size="sm" tightens the padding for dense forms.
<label class="hc-dropzone" data-size="sm"> <!-- …the same input and body… --></label>htmx usage
Section titled “htmx usage”Uploading is the
file-upload recipe’s
contract: data-hx-encoding="multipart/form-data" on the form, live
progress via installUploadProgress(), and the server’s response shape
all live there. The dropzone drops into that form unchanged — a drop
ends in the same native change as a browse.
Accessibility
Section titled “Accessibility”- Everything a keyboard or assistive-tech user needs is the native
input: label activation, focus + Enter/Space, constraint validation,
aria-invalid/aria-describedbyfrom field-errors. - Drag-and-drop is an enhancement; every outcome is reachable via browse. No ARIA is invented.
- Selected names are visible text inside the label. Pair with a toast if selection must be announced beyond that.
- Non-goals (v1): paste-to-upload, directory drops.
Theming tokens
Section titled “Theming tokens”Component tokens (in component.tokens.json) — all referencing
semantic tokens, so themes, accents (data-color) and dark mode apply
automatically:
| Token path | Purpose |
|---|---|
dropzone.bg / fg | Surface background and text. |
dropzone.border | Dashed border color. |
dropzone.hint | Hint text color. |
dropzone.radius | Corner radius. |
dropzone.padding-y / padding-x | Surface padding (data-size="sm" overrides it). |
dropzone.dragover-border / dragover-bg | Drag-over accent (follows data-color). |
dropzone.error-border | Invalid-state border. |
CSS variables
Section titled “CSS variables”Show the generated CSS variables
--hc-dropzone-bg | -fg | -border | -hint | -radius--hc-dropzone-padding-y | -padding-x--hc-dropzone-dragover-border | -dragover-bg--hc-dropzone-error-borderRelated
Section titled “Related”- File upload recipe — the multipart + progress contract this drops into unchanged.
- Field errors recipe — validation wiring on the input.
- Input — the plain control this decorates.