CarsXE

Search docs

Search pages, components, and skills.

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

Drag and drop files here or browse

PDF

No title document selected yet.

Submitting with: nothing

Custom layout from the parts

Inspection photos

Built from the parts, without the dropzone.

API Reference

PropTypeDefault
acceptRecord<string, string[]> — e.g. { "image/*": [".png", ".jpg"] }—
maxSizenumber — bytes—
minSizenumber — bytes—
maxFilesnumber — counts files already in the list—
multipleboolean — false replaces the filetrue
validator(file: File) => FileError | FileError[] | null—
disabledbooleanfalse
autoboolean — upload as soon as files are addedfalse
concurrencynumber — parallel uploads3
previewImagesbooleantrue
urlstring — built-in XMLHttpRequest upload—
method"POST" | "PUT" | "PATCH""POST"
namestring — form field for the file"file"
headersRecord<string, string>—
withCredentialsbooleanfalse
formData(file: File) => XMLHttpRequestBodyInit — replaces the default FormData—
uploader(file, { onProgress, signal, item }) => Promise<unknown> — takes precedence over url—
valueFileUploadItemData[] — controlled list—
onValueChange(files: FileUploadItemData[]) => void—
messagesPartial<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—
emptyReactNode — FileUploadAdvanced, shown with no files—
chooseLabel / uploadLabel / clearLabelReactNode — FileUploadAdvanced button labels—