Skip to content
Template Empire
Browse components

File Dropzone Queue

A free React file dropzone built on a real file input, with a validated, accessible queue.

Up to 8 files, 10 MB each. Accepts images and PDF files.

  • hero-billboard.svgimage/svg+xml · 1.2 MB
  • brand-guidelines-v3.pdfapplication/pdf · 2.8 MB
  • logo-primary.svgimage/svg+xml · 14 KB
  • launch-press-kit.pdfapplication/pdf · 860 KB

Customise

1.035
48px
0.045s
22px

Code

<InputFileDropzone value={files} onValueChange={setFiles} accept="image/*,application/pdf" label="Add project files" maxFiles={8} />

File Dropzone Queue is a free react file dropzone for Next.js built the way round that keeps it usable: a visually styled label wraps one real file input, so the operating system picker opens from a genuine activation and Tab, Enter and Space work because they are the browser's. Drag and drop is an addition on top of that, never the only way in.

Count, size and duplicate checks run before the queue is emitted, and every rejection is named in visible text and announced once in a polite region. Object URLs are created only for images, only up to the queue limit, and revoked the moment a file leaves the queue or the component unmounts; file contents are never read. Removing a row moves focus to the next Remove button, or back to the input when the last row goes, and with reduced motion enabled the drop pulse, the row stagger and the reflow are all dropped while selection, validation, previews, removal and focus stay exactly the same.

Install

Add the @te registry to your components.json once:

{
  "registries": {
    "@te": "https://templateempire.io/r/{name}.json"
  }
}

Then install the component:

npx shadcn@latest add @te/input-file-dropzone

Usage

"use client";

import { useState } from "react";

import { InputFileDropzone } from "@/components/input-file-dropzone";

export default function Page() {
  const [files, setFiles] = useState<File[]>([]);

  return (
    <InputFileDropzone
      value={files}
      onValueChange={setFiles}
      accept="image/*,application/pdf"
      label="Add project files"
      maxFiles={5}
    />
  );
}

Props

PropTypeDefaultDescription
acceptstringundefinedPassed straight to the native file input, and used to decide whether a drag looks acceptable. Client-side validation stays best effort; the browser only exposes a MIME type during a drag, never a name or size.
multiplebooleantrueNative multi-file behaviour. With it off, a drop of several files takes the first.
maxFilesnumber5Queue limit, clamped to 1-20. Files past the limit are rejected with a message naming the limit.
maxSizenumber10485760Per-file byte limit, clamped to 1KB-100MB. Checked before the queue limit, so a file that could never be accepted says why.
valueReadonlyArray<File>[]The controlled queue. The component holds no copy of its own.
onValueChange(files: File[]) => voidrequiredReceives the next queue after an add or a remove, as a fresh array the caller owns.
labelstring"Add files"The accessible label for the native input, and the visible prompt.
layout"stacked" | "inline""stacked"Wide-screen composition: stacked puts the queue under the dropzone, inline puts it alongside. Both stack on narrow screens.
dropPulsenumber1.035How far the drop target scales while a valid drag is over it, clamped to 1-1.08. One disables the pulse.
previewSizenumber48Image preview and file-icon size in CSS pixels, clamped to 36-80.
staggernumber0.045Reveal stagger between newly added rows in seconds, clamped to 0-0.12. It applies to the files that just arrived, not to their position in the queue.
radiusnumber22Dropzone and row corner radius in CSS pixels, clamped to 0-42.
classNamestringundefinedAdditional classes merged onto the field wrapper with cn().

Customisation

File Dropzone Queue declares three tokens of its own, each falling back to a global semantic token, so it retints from an ancestor without touching the component source. Geometry and timing come from props rather than tokens, because they are clamped: --dropzone-accent colours the valid drag state and every focus ring, --dropzone-panel is the dropzone and row surface, and --dropzone-error is the rejection accent.

:root {
  --dropzone-accent: var(--chart-4);
  --dropzone-panel: var(--muted);
  --dropzone-error: oklch(0.62 0.21 25);
}

<InputFileDropzone value={files} onValueChange={setFiles} layout="inline" radius={8} />

Frequently asked questions

Is File Dropzone Queue free for commercial use?
Yes. File Dropzone Queue is MIT licensed, so you can use it in personal and commercial projects without attribution.
Does it leak memory from image previews?
No. An object URL is a live handle on a file, so they are reconciled against the queue rather than created during render: one is made when an image enters the queue, and revoked the moment that file is removed, replaced or the component unmounts. Only images get one, and only up to the queue limit, so a video or an archive never holds a resource for a preview that is not shown. File contents are never read at any point.
Is File Dropzone Queue accessible for keyboard and screen reader users?
Yes, and the structure is what makes it so rather than added attributes. The drop target is a real label around a real file input, so the picker opens from a genuine activation and keyboard access is the browser's own; the dropzone is never given a fake button role. The input is described by both its constraints and its errors, the queue is a semantic list, and every row's Remove button names its own file so a column of Remove buttons is still navigable. After a removal focus moves to the next Remove button, or back to the input when the last row goes, rather than falling to the body.

Related components

Want the full page this belongs to?Template Empire builds complete Next.js templates with this motion system throughout. Browse the paid templates that pair with these free components.Explore Template Empire templates