Primitives Forms
File drop
A drop area for files. The area is a native file input stretched over it, so the drop, the picker, accept, multiple and the form value are the browser's; the behavior marks the area during a drag and lists the chosen files. Tier 1.
- Tier
- 1 · Delegated micro-behavior
- Behavior JS
- 1107 B brotli · 1244 B gzip ·
packages/· methodprimitives/ artifacts/ size.json - Platform features
<input type="file">,drop,dragenter,Intl.ListFormat,@media (scripting)- Shared listeners
- dragenter, dragleave, drop, change
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<input type="file"> stretched over a drop area
On this page
Example
Drop images or PDFs here, or choose files
<div data-slean="file-drop">
<input
id="file-drop-upload"
type="file"
name="files"
multiple
accept="image/*,.pdf"
aria-labelledby="file-drop-hint"
aria-describedby="file-drop-files"
data-slean-part="input"
/>
<p id="file-drop-hint">Drop images or PDFs here, or choose files</p>
<output id="file-drop-files" for="file-drop-upload" data-slean-part="output"></output>
</div><script lang="ts">
// With @svelte-lean/vite the register import is injected for the static data-slean="file-drop".
import '@svelte-lean/styles/file-drop.css';
let files = $state<FileList | undefined>();
</script>
<form method="post" enctype="multipart/form-data">
<div data-slean="file-drop">
<input
id="upload"
type="file"
name="files"
multiple
accept="image/*,.pdf"
bind:files
aria-labelledby="upload-hint"
aria-describedby="upload-files"
data-slean-part="input"
/>
<p id="upload-hint">Drop images or PDFs here, or choose files</p>
<output id="upload-files" for="upload" data-slean-part="output"></output>
</div>
<button disabled={!files?.length}>Upload</button>
</form>Why this implementation exists
A file input already accepts a dropped file: it becomes the input’s value and the input fires change, exactly as when the file is chosen in the picker. Drop-zone components rebuild that with dragover handlers, preventDefault() and a hidden input they fill by hand.
Here the input is the drop zone. The stylesheet stretches it, transparent, over the area, so a drop or a click anywhere on it reaches the input, and the form, the keyboard and the accessible name are unchanged. What a transparent input cannot do is show itself: the behavior marks the area while a file is dragged over it and writes the chosen names into an <output>, from three routed drag events and change. It never handles dragover, which fires continuously.
The browser owns
- the drop itself: a file dropped on a file input becomes its value and fires change
- the picker, accept, multiple, required and the form value
- the keyboard path (Enter or Space opens the picker) and the accessible name
Svelte Lean owns
- a mark on the area while a file is dragged over it (data-slean-dragging)
- the chosen files’ names in the output, as a list in the page’s language
- file-drop.css: the input stretched, transparent, over the area; the dashed area, the drag and focus states; the native input without JavaScript
Usage
Install @svelte-lean/primitives for the behavior and @svelte-lean/styles for the stylesheet. With @svelte-lean/vite the
registration is injected for every static data-slean="file-drop"; without it, import
the register module once.
npm install @svelte-lean/primitivespnpm add @svelte-lean/primitivesyarn add @svelte-lean/primitivesbun add @svelte-lean/primitivesimport '@svelte-lean/primitives/file-drop/register';import '@svelte-lean/styles/file-drop.css';Put the input, the instruction and an optional <output> in the root. Name the input with aria-labelledby pointing at the instruction, and describe it with the output.
Read the files from the input as usual: bind:files, FormData or a change listener. accept filters what the picker offers, not what can be dropped: check the type in your handler and on the server.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <div data-slean="file-drop"> | – | yes | No options. data-slean-dragging is set during a drag. |
| input | <input type="file" aria-labelledby="…"> | input | yes | Stretched over the area by the stylesheet; named by the visible instruction. |
| output | <output for="…"> | output | no | Receives the names of the chosen files. |
Runtime profile
The file drop registers dragenter, dragleave, drop and change handlers with the shared router, one listener per type for the whole page. A thousand areas keep one listener per type (tests/file-drop.test.ts).
Accessibility contract
- The input is the control: a native file input, named by the visible instruction and reached with Tab.
- Enter or Space opens the picker, which is the keyboard path; dropping is a pointer gesture with no keyboard equivalent.
- The output names the chosen files and is described by the input, so the choice is announced.
- The focus ring is drawn around the whole area while the input has keyboard focus.
Keyboard
| Key | When | Result |
|---|---|---|
| Tab | in the page | Reaches the input (the area shows the ring) |
| Enter/Space | focus on the input | Opens the file picker (native) |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
Dropping files on a file input | Widely available | Not applicable; accept does not filter dropped files |
Intl.ListFormat | Baseline 2021 | The names are joined with commas |
@media (scripting) | Baseline 2023 | The input stays stretched and invisible without JavaScript |
Without JavaScript
The input is shown as it is under @media (scripting: none): the platform's button and file name, a native drop target and the form value all work.
Server rendering
Render the markup with an empty output. Nothing is written before a drag or a change.
Before hydration
Before the behavior loads, a drop or a choice already sets the input's files; the names appear in the output from the next change on.
Styling
file-drop.css draws the dashed area, stretches the transparent input over it while scripting is enabled, marks the area during a drag (data-slean-dragging) and on :user-invalid, and moves the focus ring to the area. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-space-2 | 0.5rem | gap |
--slean-space-5 | 1.25rem | padding |
--slean-border-strong | var(--slean-neutral-8) | border |
--slean-radius-lg | 0.875rem | border-radius |
--slean-surface | oklch(100% 0 0) | background |
--slean-fg-muted | var(--slean-neutral-11) | color, border-color |
--slean-text-sm | 0.875rem | font-size |
--slean-duration-fast | 100ms | transition |
--slean-ease | cubic-bezier(0.2, 0, 0, 1) | transition |
--slean-fg | var(--slean-neutral-12) | color |
--slean-font-weight-medium | 500 | font-weight |
--slean-focus-ring | var(--slean-focus-ring-width) solid var(--slean-focus-ring-color) | outline |
--slean-focus-ring-offset | 2px | outline-offset |
--slean-accent | oklch(54% 0.19 258) | border-color |
--slean-accent-soft | oklch(95% 0.03 258) | background |
--slean-accent-soft-fg | oklch(42% 0.17 258) | color |
--slean-danger | oklch(55% 0.2 25) | border-color |
State selectors the stylesheet targets, all from the platform or ARIA: :disabled, :empty, :enabled, :focus-visible, :hover, :user-invalid.
Variant attributes: data-slean-dragging.
Compatibility notes
Dropping on a file input, the drag events and Intl.ListFormat are widely available; @media (scripting) is Baseline 2023. Uploading, previews, progress and removing single files are application code.
Testing
packages/primitives/tests/file-drop.test.tsVitest: the drag mark, disabled inputs, the list of names by language, 1000 rootsapps/playground/tests/primitives/widgets.spec.tsPlaywright: tooltip, toast, tree, range slider and number field in Chrome, with axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/file-drop/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/file-drop/contract.tstyped constants: name, tier, base, parts, options, eventspackages/primitives/src/file-drop/behavior.tsthe behavior definitionpackages/primitives/src/file-drop/register.tsthe registration modulepackages/styles/css/file-drop.cssthe optional stylesheet