Skip to content
Devix Open Source

Guide

Getting started

npm install @devix-labs/file-uploader
<label for="docs">Attachments</label>
<input id="docs" name="docs[]" type="file">
import { createFileUploader } from '@devix-labs/file-uploader';
import '@devix-labs/file-uploader/styles.css';

const uploader = createFileUploader(document.querySelector('#docs'), {
  url: '/upload',
  accept: 'image/*,.pdf',
  maxSize: '5MB',
  maxFiles: 10,
  onSuccess: (file, response) => console.log(file.file.name, JSON.parse(response)),
});

The input you point at stays the control. It is already focusable, already announced by a screen reader, and already what a form posts; the drop zone is its label. Nothing is reinvented, so nothing is lost.

Sizes say their own unit

maxSize: 5_000_000   // bytes. A bare number is always bytes.
maxSize: '5MB'       // 5,000,000
maxSize: '5MiB'      // 5,242,880 — the other thing people mean by "5 meg"
maxSize: '500 kB'

The most-reacted open issues on the uploader most projects use are four separate reports of the same confusion, because its limit is documented in bytes and read as megabytes. Here there is nothing to get wrong.

No server? It still works

Leave url out and the widget is an enhancement rather than a replacement: the files stay in the real <input type="file"> and your form posts them exactly as it would have.

createFileUploader(input, { accept: 'image/*', maxSize: '5MB' });
// Laravel, unchanged
$request->file('docs');

That also means the page works the same if the JavaScript never runs.

Why a file was turned away

Every rejected file stays in the list saying which rule it broke, and your code gets a code rather than a sentence to match:

createFileUploader(input, {
  accept: 'image/*',
  maxSize: '5MB',
  onReject: (file, problem) => {
    problem.code;     // 'too-large' | 'wrong-type' | 'too-many' | 'duplicate' | 'empty' | 'rejected'
    problem.limit;    // 5000000
    problem.actual;   // 9400000
  },
});

A rule of your own can answer with one of those codes, or with its own sentence:

createFileUploader(input, {
  check: (file) => (file.name.startsWith('IMG_') ? null : 'Only photos straight from a camera'),
});

Big files

createFileUploader(input, { url: '/upload', chunkSize: 5_000_000 });

The file goes up in pieces, so a dropped connection costs one piece rather than an hour. Before it starts, the client asks the server what it already has and skips ahead. The protocol is plain headers and a raw body — see Uploading — and a receiver is about twenty lines.

Where to go next

Updated 15 Sep 2026