File Upload
Drag-and-drop file picking built on react-dropzone, modeled on PrimeReact's FileUpload. Checks type, size and count with readable messages, previews images, and uploads with per-file progress, cancel, retry and a concurrency limit. Pass `url` to upload with the built-in XMLHttpRequest transport, or `uploader` to report progress from your own (axios, S3 presigned URLs, tus). Leave both out to only collect files for a form. Use `FileUploadAdvanced` for the full layout, or compose the parts and `useFileUploadContext` for your own.
Drag and drop files here or browse
PDF, PNG, JPG, JPEG · Up to 10 MB each · 5 files max
Installation
Import from the package. Do not run shadcn add in your app.
tsx
import { FileUploadAdvanced } from "@carsxe/design-system/components/file-upload"Usage
tsx
import * as React from "react"
import { PaperclipIcon } from "lucide-react"
import {
FileUpload,
FileUploadAdvanced,
FileUploadInput,
FileUploadItem,
FileUploadList,
FileUploadTotalProgress,
useFileUploadContext,
type FileUploader,
} from "@carsxe/design-system/components/file-upload"
import { Button } from "@carsxe/design-system/components/button"
/** Stands in for a real endpoint: reports progress in steps and honours cancel. Files named "fail" error. */
export const simulateUpload: FileUploader = (file, { onProgress, signal }) =>
new Promise((resolve, reject) => {
let sent = 0
const step = Math.max(file.size / 12, 1)
const timer = window.setInterval(() => {
sent = Math.min(sent + step, file.size)
onProgress(sent, file.size)
if (file.name.toLowerCase().includes("fail") && sent > file.size / 2) {
window.clearInterval(timer)
reject(new Error("The server rejected this file."))
} else if (sent >= file.size) {
window.clearInterval(timer)
resolve({ name: file.name })
}
}, 150)
signal.addEventListener("abort", () => {
window.clearInterval(timer)
reject(new DOMException("Cancelled", "AbortError"))
})
})
export function FileUploadDemo() {
return (
<div className="w-full max-w-xl">
<FileUploadAdvanced
uploader={simulateUpload}
accept={{
"application/pdf": [".pdf"],
"image/*": [".png", ".jpg", ".jpeg"],
}}
maxSize={10 * 1024 * 1024}
maxFiles={5}
/>
</div>
)
}
/** Docs have no upload server, so this preview simulates one. The docs snippet shows the `url` version. */
export function FileUploadNative() {
return (
<div className="w-full max-w-xl">
<FileUploadAdvanced uploader={simulateUpload} />
</div>
)
}
export function FileUploadAuto() {
const [uploaded, setUploaded] = React.useState(0)
return (
<div className="w-full max-w-xl space-y-2">
<FileUploadAdvanced
auto
uploader={simulateUpload}
accept={{ "image/*": [".png", ".jpg", ".jpeg", ".webp"] }}
maxSize={2 * 1024 * 1024}
maxFiles={4}
concurrency={2}
onUpload={() => setUploaded((count) => count + 1)}
/>
<p className="text-sm text-muted-foreground" aria-live="polite">
{uploaded} {uploaded === 1 ? "photo" : "photos"} uploaded
</p>
</div>
)
}
export function FileUploadSelectOnly() {
const [names, setNames] = React.useState<string[]>([])
return (
<form
className="w-full max-w-xl space-y-3"
onSubmit={(event) => event.preventDefault()}
>
<FileUploadAdvanced
multiple={false}
accept={{ "application/pdf": [".pdf"] }}
onValueChange={(files) => setNames(files.map((item) => item.file.name))}
empty="No title document selected yet."
/>
<p className="text-sm text-muted-foreground">
Submitting with: {names.length ? names.join(", ") : "nothing"}
</p>
</form>
)
}
function AttachButton() {
const { open, files, disabled } = useFileUploadContext()
return (
<Button variant="outline" size="sm" onClick={open} disabled={disabled}>
<PaperclipIcon data-icon="inline-start" aria-hidden="true" />
Attach {files.length ? `(${files.length})` : ""}
</Button>
)
}
export function FileUploadCustomLayout() {
return (
<FileUpload
auto
uploader={simulateUpload}
className="w-full max-w-xl rounded-2xl border bg-card p-4"
>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium">Inspection photos</p>
<p className="text-xs text-muted-foreground">
Built from the parts, without the dropzone.
</p>
</div>
<AttachButton />
</div>
<FileUploadTotalProgress />
<FileUploadList
className="grid grid-cols-1 sm:grid-cols-2"
renderItem={(item) => (
<FileUploadItem item={item} size="sm" className="h-full" />
)}
/>
<FileUploadInput />
</FileUpload>
)
}
Built-in upload with url (preview uses a simulated server)
Drag and drop files here or browse
Custom uploader reporting progress
Drag and drop files here or browse
Auto upload, images only, 2 MB and 4 files max
Drag and drop files here or browse
PNG, JPG, JPEG, WEBP · Up to 2 MB each · 4 files max
0 photos uploaded
Select only, for form submission
Custom layout from the parts
Inspection photos
Built from the parts, without the dropzone.
API Reference
| Prop | Type | Default |
|---|---|---|
| accept | Record<string, string[]> — e.g. { "image/*": [".png", ".jpg"] } | — |
| maxSize | number — bytes | — |
| minSize | number — bytes | — |
| maxFiles | number — counts files already in the list | — |
| multiple | boolean — false replaces the file | true |
| validator | (file: File) => FileError | FileError[] | null | — |
| disabled | boolean | false |
| auto | boolean — upload as soon as files are added | false |
| concurrency | number — parallel uploads | 3 |
| previewImages | boolean | true |
| url | string — built-in XMLHttpRequest upload | — |
| method | "POST" | "PUT" | "PATCH" | "POST" |
| name | string — form field for the file | "file" |
| headers | Record<string, string> | — |
| withCredentials | boolean | false |
| formData | (file: File) => XMLHttpRequestBodyInit — replaces the default FormData | — |
| uploader | (file, { onProgress, signal, item }) => Promise<unknown> — takes precedence over url | — |
| value | FileUploadItemData[] — controlled list | — |
| onValueChange | (files: FileUploadItemData[]) => void | — |
| messages | Partial<FileUploadMessages> — fileTooLarge, fileTooSmall, fileInvalidType, tooManyFiles, uploadFailed | — |
| onSelect | (items) => void | — |
| onReject | (rejections) => void | — |
| onBeforeUpload | (item) => boolean | void — return false to skip | — |
| onProgress | (item) => void | — |
| onUpload | (item, response) => void | — |
| onError | (item, error) => void | — |
| onRemove | (item) => void | — |
| onClear | () => void | — |
| onAllComplete | (items) => void | — |
| renderItem | (item, state) => ReactNode — FileUploadAdvanced and FileUploadList | — |
| empty | ReactNode — FileUploadAdvanced, shown with no files | — |
| chooseLabel / uploadLabel / clearLabel | ReactNode — FileUploadAdvanced button labels | — |

