ComponentsFile Upload
File Upload
Keep filenames in focus. Add files, review the batch, and recover without starting over.
On this page
Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.
View registry files- Lucide
(lucide-react)React SVG icons used in component controls and status indicators.
- React Aria Components
(react-aria-components)Accessible interaction primitives, including keyboard behavior, focus handling, and ARIA semantics.
Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.
Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.
Import the component into your page or component file.
import { FileUpload } from "@dethink/components";A compact intake strip, readable file rows, and progress right beneath the filename.
Project documents
Drop a mixed batch, review accepted files, and upload. Simulate a failure to try recovery without restarting the batch.
Project files
Bring the brief, notes, and references together. Review your files before uploading.
Drop files here
PDF, DOCX, PNG, JPG · Max 20 MiB each · Up to 10 files
Simulated uploads. Files stay in your browser.
Show sourceexamples/file-upload/workspace.tsx
"use client";
import { useRef, useState } from "react";
import {
Button,
FileUpload,
type FileUploadHandler,
} from "@dethink/components";
import { FileUp, ShieldCheck } from "lucide-react";
function waitForTick(signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const cancel = () => {
clearTimeout(timer);
reject(new DOMException("Cancelled", "AbortError"));
};
const timer = setTimeout(() => {
signal.removeEventListener("abort", cancel);
resolve();
}, 200);
if (signal.aborted) cancel();
else signal.addEventListener("abort", cancel, { once: true });
});
}
export function FileUploadWorkspace() {
const failNext = useRef(false);
const [armed, setArmed] = useState(false);
const upload: FileUploadHandler = async (_file, { signal, onProgress }) => {
const fail = failNext.current;
if (fail) {
failNext.current = false;
setArmed(false);
}
for (let progress = 0; progress <= 100; progress += 5) {
await waitForTick(signal);
onProgress(progress);
if (fail && progress >= 40)
throw new Error("Connection interrupted. Retry this file.");
}
};
return (
<div className="bg-background text-foreground border-border mx-auto w-full max-w-3xl overflow-hidden rounded-xl border">
<div className="border-border flex items-center justify-between gap-4 border-b px-5 py-3">
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<FileUp className="size-3.5" aria-hidden="true" />
<span>PROJECT / DOCUMENTS</span>
</div>
<span className="text-muted-foreground text-xs">Local demo</span>
</div>
<div className="p-5 sm:p-7">
<FileUpload
label="Project files"
description="Bring the brief, notes, and references together. Review your files before uploading."
multiple
accept=".pdf,.docx,.png,.jpg"
maxFiles={10}
maxFileSize={20 * 1024 * 1024}
onUpload={upload}
/>
</div>
<div className="border-border bg-muted/20 flex flex-wrap items-center justify-between gap-3 border-t px-5 py-3">
<p className="text-muted-foreground flex items-center gap-2 text-xs">
<ShieldCheck aria-hidden="true" className="size-3.5" />
Simulated uploads. Files stay in your browser.
</p>
<Button
variant="link"
size="xs"
disabled={armed}
onClick={() => {
failNext.current = true;
setArmed(true);
}}
>
{armed ? "Next upload will fail once" : "Make next upload fail"}
</Button>
</div>
</div>
);
}
export function FileUploadSingle() {
return (
<div className="mx-auto w-full max-w-lg">
<FileUpload
label="Signed agreement"
description="Choose one PDF. An invalid replacement keeps your current document."
accept=".pdf"
maxFileSize={5 * 1024 * 1024}
onUpload={async (_file, { signal, onProgress }) => {
for (let value = 0; value <= 100; value += 10) {
await waitForTick(signal);
onProgress(value);
}
}}
/>
<p className="text-muted-foreground mt-4 text-xs">
Local simulation · no files leave your browser.
</p>
</div>
);
}
export function FileUploadAutomatic() {
return (
<div className="mx-auto w-full max-w-lg">
<FileUpload
label="Image attachments"
description="Accepted images start immediately. Cancel and retry each file independently."
multiple
accept="image/png,image/jpeg,image/webp"
maxFiles={5}
maxFileSize={8 * 1024 * 1024}
autoUpload
onUpload={async (_file, { signal }) => {
for (let i = 0; i < 12; i++) await waitForTick(signal);
}}
/>
<p className="text-muted-foreground mt-4 text-xs">
Local simulation with unknown progress · no files leave your browser.
</p>
</div>
);
}Single file
Replace a document without losing it when the new selection is invalid.
Signed agreement
Choose one PDF. An invalid replacement keeps your current document.
Drop a file here
PDF · Max 5 MiB each · One file
Local simulation · no files leave your browser.
Show sourceexamples/file-upload/workspace.tsx
"use client";
import { useRef, useState } from "react";
import {
Button,
FileUpload,
type FileUploadHandler,
} from "@dethink/components";
import { FileUp, ShieldCheck } from "lucide-react";
function waitForTick(signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const cancel = () => {
clearTimeout(timer);
reject(new DOMException("Cancelled", "AbortError"));
};
const timer = setTimeout(() => {
signal.removeEventListener("abort", cancel);
resolve();
}, 200);
if (signal.aborted) cancel();
else signal.addEventListener("abort", cancel, { once: true });
});
}
export function FileUploadWorkspace() {
const failNext = useRef(false);
const [armed, setArmed] = useState(false);
const upload: FileUploadHandler = async (_file, { signal, onProgress }) => {
const fail = failNext.current;
if (fail) {
failNext.current = false;
setArmed(false);
}
for (let progress = 0; progress <= 100; progress += 5) {
await waitForTick(signal);
onProgress(progress);
if (fail && progress >= 40)
throw new Error("Connection interrupted. Retry this file.");
}
};
return (
<div className="bg-background text-foreground border-border mx-auto w-full max-w-3xl overflow-hidden rounded-xl border">
<div className="border-border flex items-center justify-between gap-4 border-b px-5 py-3">
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<FileUp className="size-3.5" aria-hidden="true" />
<span>PROJECT / DOCUMENTS</span>
</div>
<span className="text-muted-foreground text-xs">Local demo</span>
</div>
<div className="p-5 sm:p-7">
<FileUpload
label="Project files"
description="Bring the brief, notes, and references together. Review your files before uploading."
multiple
accept=".pdf,.docx,.png,.jpg"
maxFiles={10}
maxFileSize={20 * 1024 * 1024}
onUpload={upload}
/>
</div>
<div className="border-border bg-muted/20 flex flex-wrap items-center justify-between gap-3 border-t px-5 py-3">
<p className="text-muted-foreground flex items-center gap-2 text-xs">
<ShieldCheck aria-hidden="true" className="size-3.5" />
Simulated uploads. Files stay in your browser.
</p>
<Button
variant="link"
size="xs"
disabled={armed}
onClick={() => {
failNext.current = true;
setArmed(true);
}}
>
{armed ? "Next upload will fail once" : "Make next upload fail"}
</Button>
</div>
</div>
);
}
export function FileUploadSingle() {
return (
<div className="mx-auto w-full max-w-lg">
<FileUpload
label="Signed agreement"
description="Choose one PDF. An invalid replacement keeps your current document."
accept=".pdf"
maxFileSize={5 * 1024 * 1024}
onUpload={async (_file, { signal, onProgress }) => {
for (let value = 0; value <= 100; value += 10) {
await waitForTick(signal);
onProgress(value);
}
}}
/>
<p className="text-muted-foreground mt-4 text-xs">
Local simulation · no files leave your browser.
</p>
</div>
);
}
export function FileUploadAutomatic() {
return (
<div className="mx-auto w-full max-w-lg">
<FileUpload
label="Image attachments"
description="Accepted images start immediately. Cancel and retry each file independently."
multiple
accept="image/png,image/jpeg,image/webp"
maxFiles={5}
maxFileSize={8 * 1024 * 1024}
autoUpload
onUpload={async (_file, { signal }) => {
for (let i = 0; i < 12; i++) await waitForTick(signal);
}}
/>
<p className="text-muted-foreground mt-4 text-xs">
Local simulation with unknown progress · no files leave your browser.
</p>
</div>
);
}Selection only
Use the queue as a staging field when the host owns submission.
Supporting documents
Add your files. Review and remove them before continuing.
Drop files here
PDF, DOCX, PNG, JPG · Max 20 MiB each · Up to 10 files
Show sourceexamples/file-upload/basic.tsx
"use client";
import { FileUpload } from "@dethink/components";
export function FileUploadBasic() {
return (
<FileUpload
label="Supporting documents"
description="Add your files. Review and remove them before continuing."
multiple
accept=".pdf,.docx,.png,.jpg"
maxFiles={10}
maxFileSize={20 * 1024 * 1024}
className="mx-auto w-full max-w-xl"
/>
);
}Automatic attachments
Opt into immediate uploads and show honest indeterminate feedback when percent is unavailable.
Image attachments
Accepted images start immediately. Cancel and retry each file independently.
Drop files here
image/png, image/jpeg, image/webp · Max 8 MiB each · Up to 5 files
Local simulation with unknown progress · no files leave your browser.
Show sourceexamples/file-upload/workspace.tsx
"use client";
import { useRef, useState } from "react";
import {
Button,
FileUpload,
type FileUploadHandler,
} from "@dethink/components";
import { FileUp, ShieldCheck } from "lucide-react";
function waitForTick(signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const cancel = () => {
clearTimeout(timer);
reject(new DOMException("Cancelled", "AbortError"));
};
const timer = setTimeout(() => {
signal.removeEventListener("abort", cancel);
resolve();
}, 200);
if (signal.aborted) cancel();
else signal.addEventListener("abort", cancel, { once: true });
});
}
export function FileUploadWorkspace() {
const failNext = useRef(false);
const [armed, setArmed] = useState(false);
const upload: FileUploadHandler = async (_file, { signal, onProgress }) => {
const fail = failNext.current;
if (fail) {
failNext.current = false;
setArmed(false);
}
for (let progress = 0; progress <= 100; progress += 5) {
await waitForTick(signal);
onProgress(progress);
if (fail && progress >= 40)
throw new Error("Connection interrupted. Retry this file.");
}
};
return (
<div className="bg-background text-foreground border-border mx-auto w-full max-w-3xl overflow-hidden rounded-xl border">
<div className="border-border flex items-center justify-between gap-4 border-b px-5 py-3">
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<FileUp className="size-3.5" aria-hidden="true" />
<span>PROJECT / DOCUMENTS</span>
</div>
<span className="text-muted-foreground text-xs">Local demo</span>
</div>
<div className="p-5 sm:p-7">
<FileUpload
label="Project files"
description="Bring the brief, notes, and references together. Review your files before uploading."
multiple
accept=".pdf,.docx,.png,.jpg"
maxFiles={10}
maxFileSize={20 * 1024 * 1024}
onUpload={upload}
/>
</div>
<div className="border-border bg-muted/20 flex flex-wrap items-center justify-between gap-3 border-t px-5 py-3">
<p className="text-muted-foreground flex items-center gap-2 text-xs">
<ShieldCheck aria-hidden="true" className="size-3.5" />
Simulated uploads. Files stay in your browser.
</p>
<Button
variant="link"
size="xs"
disabled={armed}
onClick={() => {
failNext.current = true;
setArmed(true);
}}
>
{armed ? "Next upload will fail once" : "Make next upload fail"}
</Button>
</div>
</div>
);
}
export function FileUploadSingle() {
return (
<div className="mx-auto w-full max-w-lg">
<FileUpload
label="Signed agreement"
description="Choose one PDF. An invalid replacement keeps your current document."
accept=".pdf"
maxFileSize={5 * 1024 * 1024}
onUpload={async (_file, { signal, onProgress }) => {
for (let value = 0; value <= 100; value += 10) {
await waitForTick(signal);
onProgress(value);
}
}}
/>
<p className="text-muted-foreground mt-4 text-xs">
Local simulation · no files leave your browser.
</p>
</div>
);
}
export function FileUploadAutomatic() {
return (
<div className="mx-auto w-full max-w-lg">
<FileUpload
label="Image attachments"
description="Accepted images start immediately. Cancel and retry each file independently."
multiple
accept="image/png,image/jpeg,image/webp"
maxFiles={5}
maxFileSize={8 * 1024 * 1024}
autoUpload
onUpload={async (_file, { signal }) => {
for (let i = 0; i < 12; i++) await waitForTick(signal);
}}
/>
<p className="text-muted-foreground mt-4 text-xs">
Local simulation with unknown progress · no files leave your browser.
</p>
</div>
);
}| Prop | What it does | Default |
|---|---|---|
label / descriptionstring | Visible heading and optional purpose/instructions. | "Attachments" / — |
multipleboolean | Accumulate multiple files; single mode supports explicit replacement. | false |
acceptstring | Comma-separated extensions, MIME types, or wildcards; checked for picker and drop. | Unrestricted |
maxFileSize / maxFilesnumber | Inclusive per-file byte limit and multiple-mode count limit. Labels use KiB/MiB. | Unrestricted |
onUpload(file, { id, signal, onProgress }) => Promise<void> | Your upload transport. Resolution marks success; rejection enables retry. Honor the AbortSignal. | Not set |
autoUploadboolean | Start accepted files immediately when onUpload is present. Otherwise review first. | false |
onFilesChange(entries: readonly FileUploadEntry[]) => void | Notification of local selection and transfer states. Not a controlled value setter. | Not set |
disabledboolean | Blocks new selection, removal and starting uploads. Active uploads can still be cancelled. | false |
classNamestring | Customize the root. Layout and states use semantic tokens and data-slot hooks. | Not set |
Picker and drop share extension, MIME, size, count and duplicate checks. Valid files stay in the queue; rejected files receive individual explanations. Uploads run three at a time. Report real percentages through onProgress; resolving the callback marks success. Cancellation aborts its signal, and late results cannot overwrite a newer attempt. Removing an uploaded file only removes the local entry.
Choose files is always available as an alternative to dragging. Controls include the filename in their accessible name. Batch changes and terminal results are announced without reading every progress tick. Test your complete form with a keyboard and screen reader. Error text wraps, and actions remain reachable on small screens.
FileUpload does not provide a storage service. The application owns authentication, server-side content validation, upload requests and remote deletion. Client metadata and the accept attribute are usability aids. Only local raster images receive previews; object URLs are released when no longer needed. The examples simulate uploads entirely in your browser.