sveltelean Primitives
Versionv0.2.0 GitHub

Example

An upload area

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>
Drag a file from your desktop onto the area, or click it (or focus it and press Enter) to open the picker. Nothing is uploaded: the files stay in the input, as they would in a 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/primitives
+layout.svelte or any client module
import '@svelte-lean/primitives/file-drop/register';
stylesheets
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

PartElementdata-slean-partRequiredNotes
root<div data-slean="file-drop">–yesNo options. data-slean-dragging is set during a drag.
input<input type="file" aria-labelledby="…">inputyesStretched over the area by the stylesheet; named by the visible instruction.
output<output for="…">outputnoReceives 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

KeyWhenResult
Tabin the pageReaches the input (the area shows the ring)
Enter/Spacefocus on the inputOpens the file picker (native)

Platform features

FeatureBaselineOutside the target
Dropping files on a file inputWidely availableNot applicable; accept does not filter dropped files
Intl.ListFormatBaseline 2021The names are joined with commas
@media (scripting)Baseline 2023The 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:

TokenDefault (light)Applies to
--slean-space-20.5remgap
--slean-space-51.25rempadding
--slean-border-strongvar(--slean-neutral-8)border
--slean-radius-lg0.875remborder-radius
--slean-surfaceoklch(100% 0 0)background
--slean-fg-mutedvar(--slean-neutral-11)color, border-color
--slean-text-sm0.875remfont-size
--slean-duration-fast100mstransition
--slean-easecubic-bezier(0.2, 0, 0, 1)transition
--slean-fgvar(--slean-neutral-12)color
--slean-font-weight-medium500font-weight
--slean-focus-ringvar(--slean-focus-ring-width) solid var(--slean-focus-ring-color)outline
--slean-focus-ring-offset2pxoutline-offset
--slean-accentoklch(54% 0.19 258)border-color
--slean-accent-softoklch(95% 0.03 258)background
--slean-accent-soft-fgoklch(42% 0.17 258)color
--slean-dangeroklch(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

Source