Skip to main content
Solid Primitives 2

Primitives for uploading files.

StageCategoryVersionLast UpdatedDemo
3Browser APIs1.0.0-next.3 (next)Aug 12, 2026Demo →
Terminal window
npm i @solid-primitives/upload@next

Primitives for file picking, drag-and-drop zones, and XHR uploads with progress tracking.

  • createFilePicker — opens the OS file-picker and exposes selected files reactively
  • createFileUploader — uploads files with reactive progress, status, and error; transport is passed in explicitly
  • fileSender — XHR transport factory for createFileUploader (tree-shakeable)
  • fileUploader — ref callback factory for <input type="file"> elements
  • createDropzone — reactive drag-and-drop zone with full drag-event callbacks
  • dropzone — ref callback factory variant of createDropzone

createFilePicker

Opens the OS file-picker dialog when called and exposes the selected files, loading state, and any callback error as reactive signals.

import { createFilePicker } from "@solid-primitives/upload";
// Single file
const { files, isLoading, error, selectFiles } = createFilePicker();
// Multiple files with MIME filter
const { files, isLoading, error, selectFiles } = createFilePicker({
multiple: true,
accept: "image/*",
});
// Open the picker and do something with the selection
selectFiles(async files => {
await uploadToServer(files);
});

Returned object:

NameTypeDescription
filesAccessor<UploadFile[]>Reactive list of selected files; updated on every selection
errorAccessor<unknown>Error thrown by the last selectFiles callback; null if none
isLoadingAccessor<boolean>true while the selectFiles callback is pending
selectFiles(callback?: UserCallback) => voidOpens the file-picker and runs the optional callback on change
removeFile(fileName: string) => voidRemoves a single file from the list by name
clearFiles() => voidClears all selected files

Note: removeFile matches by file name. If the list contains duplicate file names, only the first match is removed.

Options:

OptionTypeDefaultDescription
acceptstring""Comma-separated list of accepted file types (passed to <input accept>). E.g. "image/*", ".pdf,.doc"
multiplebooleanfalseAllow selecting more than one file at once

Usage example — combined picker + uploader:

import { createFilePicker, createFileUploader, fileSender } from "@solid-primitives/upload";
const { selectFiles } = createFilePicker({ multiple: true, accept: "image/*" });
const { upload, files, progress, status } = createFileUploader(fileSender("/api/upload"));
<button onClick={() => selectFiles(fs => upload(fs))} disabled={status() === "uploading"}>
Select & upload
</button>
<Show when={status() === "uploading"}>
<progress value={progress().percentage} max={100} />
<span>{progress().percentage}%</span>
</Show>
<For each={files}>{f =>
<div>
{f.file.name}
<Show when={f.status === "error"}>
<span> — failed: {String(f.error)}</span>
</Show>
</div>
}</For>

createFileUploader

Uploads files with reactive per-file and aggregate progress, status, and error tracking. Each file in a batch gets its own parallel request. The transport is passed in explicitly — use the bundled fileSender factory for XHR, or supply your own. Keeping them separate lets bundlers tree-shake fileSender when it is not needed.

import { createFileUploader, fileSender } from "@solid-primitives/upload";
const { upload, files, progress, status, abort } = createFileUploader(fileSender("/api/upload"));
// upload() dispatches one request per file (in parallel); resolves when all settle
const results = await upload(myFiles);

Returned object:

NameTypeDescription
upload(files: UploadFile[]) => Promise<unknown[]>Send files in parallel; resolves when all settle
filesreadonly FileUploadEntry[]Store array — per-file progress, status, error, and response
progressAccessor<UploadProgress>Aggregate { loaded, total, percentage } across all files
statusAccessor<UploadStatus>Aggregate status: uploading > error > aborted > success > idle
abort() => voidCancel all in-flight uploads
removeFile(fileName: string) => voidRemove one entry by name; aborts all in-flight uploads
clearFiles() => voidRemove all entries; aborts all in-flight uploads

Note: removeFile matches by file.name. If a batch contains duplicate file names, only the first match is removed. Avoid batching files with duplicate names when using removeFile.

Each entry in files has the shape:

type FileUploadEntry = {
file: UploadFile;
progress: UploadProgress; // { loaded, total, percentage }
status: UploadStatus; // "idle" | "uploading" | "success" | "error" | "aborted"
error: unknown; // error from a failed upload; null otherwise
response: unknown; // parsed server response on success; null otherwise
};

Read files[i] directly in JSX for fine-grained per-file reactivity — only the row that changed re-renders:

<For each={files}>
{f => (
<div>
{f.file.name} — {f.progress.percentage}%
<Show when={f.status === "error"}>
<span>Error: {String(f.error)}</span>
</Show>
</div>
)}
</For>

Calling upload again while one is in-flight cancels the previous upload and resets all per-file state. Use abort() to cancel without starting a new one.

fileSender

Factory that creates a SendFunction backed by XHR. Imported separately so it can be tree-shaken when unused.

import { fileSender } from "@solid-primitives/upload";
// Basic
createFileUploader(fileSender("/api/upload"));
// With options
createFileUploader(
fileSender("/api/upload", { fieldName: "attachment", headers: { "X-Auth": token } }),
);

Options:

OptionTypeDefaultDescription
fieldNamestring"file"FormData field name used for each file
headersRecord<string, string>{}Additional request headers (do not set Content-Type; the browser sets it for FormData)

Custom SendFunction:

Provide your own transport — fetch, WebSocket, a test double, etc. The function is called once per file and receives the file, a progress callback, and an AbortSignal. It must return a Promise resolving with the server response or rejecting on failure. Reject with new DOMException("...", "AbortError") when the signal fires so that file's status transitions to "aborted".

const { upload, progress, status } = createFileUploader(async (file, onProgress, signal) => {
const body = new FormData();
body.append("file", file.file, file.name);
const res = await fetch("/api/upload", { method: "POST", body, signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
});

fileUploader

A ref callback factory for wiring an existing <input type="file"> element into your own reactive state. Use this when you want full control over the input element's markup (e.g. for custom styling).

import { fileUploader } from "@solid-primitives/upload";
import { createSignal } from "solid-js";
import type { UploadFile } from "@solid-primitives/upload";
const [files, setFiles] = createSignal<UploadFile[]>([]);
const [uploadError, setUploadError] = createSignal<unknown>(null);
<input
type="file"
multiple
accept="image/*"
ref={fileUploader({
userCallback: async fs => {
await uploadToServer(fs);
},
setFiles,
onError: err => setUploadError(err),
})}
/>;

If onError is omitted, a rejection from userCallback propagates as an unhandled promise rejection.

Options:

OptionTypeDescription
userCallbackUserCallbackCalled with the parsed files on every change event
setFilesSetter<UploadFile[]>Receives the parsed UploadFile[] on every change
onError(error: unknown) => voidCalled when userCallback throws; defaults to rethrowing

createDropzone

A reactive drag-and-drop zone. Attach it to any element via the ref callback and respond to the full set of drag lifecycle events.

import { createDropzone, createFileUploader, fileSender } from "@solid-primitives/upload";
const { upload, progress, status } = createFileUploader(fileSender("/api/upload"));
const { ref, files, isDragging, error } = createDropzone({
onDrop: files => upload(files),
});
<div
ref={ref}
style={{
background: isDragging() ? "lightblue" : "lightgray",
padding: "2rem",
border: "2px dashed #999",
}}
>
<Show when={status() === "uploading"} fallback="Drop files here">
Uploading… {progress().percentage}%
</Show>
<Show when={error()}>
<p>Error: {String(error())}</p>
</Show>
<For each={files()}>{file => <p>{file.name}</p>}</For>
</div>;

Returned object:

NameTypeDescription
ref(el: T) => voidRef callback — pass to the ref prop of the drop target element
filesAccessor<UploadFile[]>Reactive list of the most recently dropped files
errorAccessor<unknown>Error thrown by the last onDrop callback; null if none
isLoadingAccessor<boolean>true while the onDrop callback is pending
isDraggingAccessor<boolean>true while a drag is active over the element
removeFile(fileName: string) => voidRemoves a single file from the list by name
clearFiles() => voidClears all dropped files

Note: removeFile matches by file name. If the list contains duplicate file names, only the first match is removed.

Options (all optional):

CallbackFires when…
onDropFiles are dropped; isLoading is true while it awaits
onDragStartA drag operation begins
onDragEnterA dragged item enters the element
onDragEndA drag operation ends
onDragLeaveA dragged item leaves the element
onDragOverAn item is dragged continuously over the element
onDragAny drag event fires on the element

All callbacks have signature (files: UploadFile[]) => void | Promise<void>. isLoading tracks only the onDrop callback — drag-movement events are fire-and-forget.

dropzone

A ref callback factory variant of createDropzone. Returns a single value that is both the ref callback and the reactive state object — use it directly as a ref while reading .files, .isDragging, etc. from the same reference. Mirrors the fileUploader pattern.

import { dropzone, createFileUploader, fileSender } from "@solid-primitives/upload";
const { upload, progress, status } = createFileUploader(fileSender("/api/upload"));
<div
ref={dropzone({
onDrop: files => upload(files),
})}
style={{
background: dz.isDragging() ? "lightblue" : "lightgray",
padding: "2rem",
border: "2px dashed #999",
}}
>
<Show when={status() === "uploading"} fallback="Drop files here">
Uploading… {progress().percentage}%
</Show>
<For each={dz.files()}>{file => <p>{file.name}</p>}</For>
</div>;

The returned value is a function (the ref callback) with all createDropzone state properties attached directly to it — files, error, isLoading, isDragging, removeFile, and clearFiles. Accepts the same DropzoneOptions as createDropzone.

Types

type UploadFile = {
source: string; // blob URL from URL.createObjectURL
name: string;
size: number;
file: File;
};
type UploadStatus = "idle" | "uploading" | "success" | "error" | "aborted";
type UploadProgress = {
loaded: number;
total: number;
percentage: number; // 0–100
};
type SendFunction = (
file: UploadFile,
onProgress: (progress: UploadProgress) => void,
signal: AbortSignal,
) => Promise<unknown>;
type FileUploadEntry = {
file: UploadFile;
progress: UploadProgress;
status: UploadStatus;
error: unknown;
response: unknown;
};
type UserCallback = (files: UploadFile[]) => void | Promise<void>;
type FilePickerOptions = {
accept?: string;
multiple?: boolean;
};
type FileSenderOptions = {
fieldName?: string;
headers?: Record<string, string>;
};
type FileUploaderDirective = {
userCallback: UserCallback;
setFiles: Setter<UploadFile[]>;
onError?: (error: unknown) => void;
};

SSR

All primitives are SSR-safe. On the server they return no-op stubs so components render without errors.

Changelog

See CHANGELOG.md

Solid Primitives 2High-quality reactive primitives for building applications in Solid2
Community
githubdiscord