Validation

Types, sizes, counts, dimensions, durations, duplicates and custom rules.

"use client"

import { type FileValidator, s3Adapter } from "@uploadcn/core"

import { demoTransport } from "@/examples/_demo"
import {
  Upload,
  UploadDropzone,
  UploadDropzoneDescription,
  UploadDropzoneHeader,
  UploadDropzoneMedia,
  UploadDropzoneTitle,
  UploadQueue,
} from "@/components/ui/upload"

const adapter = s3Adapter({
  endpoint: "/api/upload",
  transport: demoTransport, // docs only, see examples/_demo.ts
})

/** Custom rules can be sync or async (e.g. ask your API if the name is taken). */
const noSpacesInName: FileValidator = (file) =>
  file.name.includes(" ") ? "File names can't contain spaces" : null

export default function FileRestrictionsExample() {
  return (
    <Upload
      adapter={adapter}
      accept={["image/png", "image/jpeg"]}
      maxSize={2 * 1000 * 1000}
      maxFiles={3}
      image={{ minWidth: 400, minHeight: 400 }}
      validate={noSpacesInName}
      duplicates="reject"
    >
      <UploadDropzone>
        <UploadDropzoneHeader>
          <UploadDropzoneMedia variant="icon" />
          <UploadDropzoneTitle>
            PNG or JPEG, at least 400×400px
          </UploadDropzoneTitle>
          <UploadDropzoneDescription>
            Max 2 MB each · up to 3 files · no spaces in names · no duplicates
          </UploadDropzoneDescription>
        </UploadDropzoneHeader>
      </UploadDropzone>
      <UploadQueue />
    </Upload>
  )
}
<Upload
  adapter={adapter}
  accept={["image/png", "image/jpeg"]}
  maxSize={10 * 1024 * 1024}
  minSize={1024}
  maxFiles={5}
  image={{ minWidth: 400, minHeight: 400 }}
  media={{ maxDuration: 600 }}
  duplicates="reject"
  validate={[noSpaces, uniqueNameOnServer]}
/>
RuleOptionIssue code
Typeaccept, MIME types, wildcards or extensionsfile-invalid-type
SizemaxSize, minSizefile-too-large, file-too-small
CountmaxFilestoo-many-files
Minimum countminFiles + validateFileCounttoo-few-files
Image dimensionsimage: { minWidth, minHeight, maxWidth, maxHeight }image-too-small, image-too-large
Audio/video durationmedia: { minDuration, maxDuration }media-too-short, media-too-long
Duplicatesduplicates: "reject" | "replace" | "allow"duplicate
Customvalidatecustom (or your own)

Rejected files appear in the list as rejected, with item.issues describing why. UploadError renders the messages and the live region announces them.

When the OS doesn't report a MIME type (common for .md, .csv and others), the type is inferred from the extension.

Custom and async validators

Return a message (or { code, message }) to reject; return nothing to accept. Validators can be async and receive an AbortSignal that fires if the file is removed.

import type { FileValidator } from "@uploadcn/core"

const noSpaces: FileValidator = (file) =>
  file.name.includes(" ") ? "File names can't contain spaces" : null

const uniqueNameOnServer: FileValidator = async (file, { signal }) => {
  const response = await fetch(`/api/files/exists?name=${encodeURIComponent(file.name)}`, { signal })
  const { exists } = await response.json()
  return exists ? "A file with this name already exists" : null
}

Cheap checks run first; image, media and custom validators only run if they pass.

Minimum files

minFiles is a form-level rule. Check it on submit:

const issue = validateFileCount(uploadedKeys.length, { minFiles: 1, maxFiles: 3 })

Without uploading

const { validate } = useFileValidation({ accept: ".pdf", maxSize: 5e6 })
const { accepted, rejected } = await validate(files)

Client-side validation is for user experience. Always enforce limits on the server, createUploadRoute checks size and type and locks them into the signature.

On this page