diff --git a/.changeset/upload-solid-2-migration.md b/.changeset/upload-solid-2-migration.md
new file mode 100644
index 000000000..8df0b2847
--- /dev/null
+++ b/.changeset/upload-solid-2-migration.md
@@ -0,0 +1,10 @@
+---
+"@solid-primitives/upload": minor
+---
+
+Migrate to Solid.js 2.0 (beta.14)
+
+- Updated peer dependencies to `solid-js@^2.0.0-beta.14` and `@solidjs/web@^2.0.0-beta.14`
+- `isServer` is now imported from `@solidjs/web` (moved out of `solid-js/web`)
+- `createDropzone`: now returns a `setRef` ref callback; event listeners are attached when the ref is assigned and cleaned up via `onCleanup` registered through `runWithOwner` back into the reactive owner scope
+- `fileUploader`: replaced the `use:fileUploader` directive (removed in Solid 2.0) with a **ref callback factory** — use `ref={fileUploader(opts)}` instead of `use:fileUploader={opts}`
diff --git a/packages/upload/README.md b/packages/upload/README.md
index d5dbd07c1..bb2d53805 100644
--- a/packages/upload/README.md
+++ b/packages/upload/README.md
@@ -8,7 +8,14 @@
[](https://www.npmjs.com/package/@solid-primitives/upload)
[](https://github.com/solidjs-community/solid-primitives#contribution-process)
-Primitive to make uploading files and making dropzones easier.
+Primitives for file picking, drag-and-drop zones, and XHR uploads with progress tracking.
+
+- [`createFilePicker`](#createfilepicker) — opens the OS file-picker and exposes selected files reactively
+- [`createFileUploader`](#createfileuploader) — uploads files with reactive progress, status, and error; transport is passed in explicitly
+- [`fileSender`](#filesender) — XHR transport factory for `createFileUploader` (tree-shakeable)
+- [`fileUploader`](#fileuploader) — ref callback factory for `` elements
+- [`createDropzone`](#createdropzone) — reactive drag-and-drop zone with full drag-event callbacks
+- [`dropzone`](#dropzone) — ref callback factory variant of `createDropzone`
## Installation
@@ -16,62 +23,344 @@ Primitive to make uploading files and making dropzones easier.
npm install @solid-primitives/upload
# or
yarn add @solid-primitives/upload
+# or
+pnpm add @solid-primitives/upload
+```
+
+## `createFilePicker`
+
+Opens the OS file-picker dialog when called and exposes the selected files, loading state, and any callback error as reactive signals.
+
+```ts
+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:**
+
+| Name | Type | Description |
+| ------------- | ----------------------------------- | ---------------------------------------------------------------- |
+| `files` | `Accessor` | Reactive list of selected files; updated on every selection |
+| `error` | `Accessor` | Error thrown by the last `selectFiles` callback; `null` if none |
+| `isLoading` | `Accessor` | `true` while the `selectFiles` callback is pending |
+| `selectFiles` | `(callback?: UserCallback) => void` | Opens the file-picker and runs the optional callback on change |
+| `removeFile` | `(fileName: string) => void` | Removes a single file from the list by name |
+| `clearFiles` | `() => void` | Clears all selected files |
+
+> **Note:** `removeFile` matches by file name. If the list contains duplicate file names, only the first match is removed.
+
+**Options:**
+
+| Option | Type | Default | Description |
+| ---------- | --------- | ------- | --------------------------------------------------------------------------------------------------------- |
+| `accept` | `string` | `""` | Comma-separated list of accepted file types (passed to ``). E.g. `"image/*"`, `".pdf,.doc"` |
+| `multiple` | `boolean` | `false` | Allow selecting more than one file at once |
+
+**Usage example — combined picker + uploader:**
+
+```tsx
+import { createFilePicker, createFileUploader, fileSender } from "@solid-primitives/upload";
+
+const { selectFiles } = createFilePicker({ multiple: true, accept: "image/*" });
+const { upload, files, progress, status } = createFileUploader(fileSender("/api/upload"));
+
+
+
+
+
+ {progress().percentage}%
+
+
+{f =>
+
+}
```
-### use:fileUploader directive
+**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.
```ts
+import { fileSender } from "@solid-primitives/upload";
+
+// Basic
+createFileUploader(fileSender("/api/upload"));
+
+// With options
+createFileUploader(fileSender("/api/upload", { fieldName: "attachment", headers: { "X-Auth": token } }));
+```
+
+**Options:**
+
+| Option | Type | Default | Description |
+| ----------- | ------------------------ | -------- | ------------------------------------------------------------------------------------- |
+| `fieldName` | `string` | `"file"` | FormData field name used for each file |
+| `headers` | `Record` | `{}` | 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"`.
+
+```ts
+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 `` element into your own reactive state. Use this when you want full control over the input element's markup (e.g. for custom styling).
+
+```tsx
+import { fileUploader } from "@solid-primitives/upload";
+import { createSignal } from "solid-js";
+import type { UploadFile } from "@solid-primitives/upload";
+
const [files, setFiles] = createSignal([]);
+const [uploadError, setUploadError] = createSignal(null);
fs.forEach(f => console.log(f)),
+ accept="image/*"
+ ref={fileUploader({
+ userCallback: async fs => {
+ await uploadToServer(fs);
+ },
setFiles,
- }}
+ onError: err => setUploadError(err),
+ })}
/>;
```
-### [createDropzone](#createdropzone)
+If `onError` is omitted, a rejection from `userCallback` propagates as an unhandled promise rejection.
+
+**Options:**
+
+| Option | Type | Description |
+| -------------- | -------------------------- | --------------------------------------------------------- |
+| `userCallback` | `UserCallback` | Called with the parsed files on every `change` event |
+| `setFiles` | `Setter` | Receives the parsed `UploadFile[]` on every change |
+| `onError` | `(error: unknown) => void` | Called 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.
+
+```tsx
+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),
+});
-```html
```
+**Returned object:**
+
+| Name | Type | Description |
+| ------------ | ---------------------------- | ---------------------------------------------------------------- |
+| `ref` | `(el: T) => void` | Ref callback — pass to the `ref` prop of the drop target element |
+| `files` | `Accessor` | Reactive list of the most recently dropped files |
+| `error` | `Accessor` | Error thrown by the last `onDrop` callback; `null` if none |
+| `isLoading` | `Accessor` | `true` while the `onDrop` callback is pending |
+| `isDragging` | `Accessor` | `true` while a drag is active over the element |
+| `removeFile` | `(fileName: string) => void` | Removes a single file from the list by name |
+| `clearFiles` | `() => void` | Clears 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):**
+
+| Callback | Fires when… |
+| ------------- | -------------------------------------------------------- |
+| `onDrop` | Files are dropped; `isLoading` is `true` while it awaits |
+| `onDragStart` | A drag operation begins |
+| `onDragEnter` | A dragged item enters the element |
+| `onDragEnd` | A drag operation ends |
+| `onDragLeave` | A dragged item leaves the element |
+| `onDragOver` | An item is dragged continuously over the element |
+| `onDrag` | Any drag event fires on the element |
+
+All callbacks have signature `(files: UploadFile[]) => void | Promise`. `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.
+
+```tsx
+import { dropzone, createFileUploader, fileSender } from "@solid-primitives/upload";
+
+const { upload, progress, status } = createFileUploader(fileSender("/api/upload"));
+
+