Repo

A dropzone lets people add files by dragging them onto a box, or by clicking it. It is a real <input type="file"> stretched over the box, so it works with a keyboard, a screen reader and a form, and the browser handles the drop.

The dropzone adds three things the browser does not:

The dropzone only picks files. You still read them from the input (input.files, or a change event) and upload them yourself.

Usage

<div class="zui-dropzone" data-zui-dropzone data-max-size="5242880">
<label class="zui-dropzone-box">
  <input class="zui-dropzone-input" type="file" multiple accept="image/*,.pdf" />
  <i class="ph ph-upload-simple zui-dropzone-icon" aria-hidden="true"></i>
  <span class="zui-dropzone-title">Drop files here, or click to browse</span>
  <span class="zui-dropzone-hint">Images or PDF, up to 5 MB each</span>
</label>
<p class="zui-dropzone-status" role="status"></p>
</div>

Drag a file from your desktop onto the box to see the highlight. Drop a .zip to see it refused.

Plain HTML

The class names alone give you the look, the click and the drop. The highlight, the type and size check and the status line need a script. The framework wrappers load it for you. In plain HTML, import it once, with a bundler such as Vite:

import { initDropzones } from '@mrmartineau/zui/dropzone'

initDropzones()

It uses one set of listeners on the document, so it covers every dropzone on the page, including ones added later. Set the size limit in bytes with data-max-size on the element that has data-zui-dropzone. Set the types with accept on the input.

Single file

Leave out multiple. The input then holds one file.

<div class="zui-dropzone" data-zui-dropzone style="width: 100%;">
<label class="zui-dropzone-box">
  <input class="zui-dropzone-input" type="file" accept="image/png,image/jpeg" />
  <i class="ph ph-image zui-dropzone-icon" aria-hidden="true"></i>
  <span class="zui-dropzone-title">Add a cover image</span>
  <span class="zui-dropzone-hint">PNG or JPG</span>
</label>
<p class="zui-dropzone-status" role="status"></p>
</div>

In a form

Give the input a name and an id, and put the dropzone in a Field with a label that points at that id, like any other control. The input then has two labels: yours ("Attachments") and the dropzone's own text, so a screen reader says both.

<form class="zui-field" method="post" enctype="multipart/form-data">
<label class="zui-label" for="attachments">Attachments</label>
<div class="zui-dropzone" data-zui-dropzone>
  <label class="zui-dropzone-box">
    <input class="zui-dropzone-input" type="file" id="attachments" name="attachments" multiple />
    <i class="ph ph-upload-simple zui-dropzone-icon" aria-hidden="true"></i>
    <span class="zui-dropzone-title">Drop files here, or click to browse</span>
  </label>
  <p class="zui-dropzone-status" role="status"></p>
</div>
</form>

Disabled

<div class="zui-dropzone" data-zui-dropzone style="width: 100%;">
<label class="zui-dropzone-box">
  <input class="zui-dropzone-input" type="file" disabled />
  <i class="ph ph-upload-simple zui-dropzone-icon" aria-hidden="true"></i>
  <span class="zui-dropzone-title">Uploads are turned off</span>
</label>
<p class="zui-dropzone-status" role="status"></p>
</div>

Shape

shape takes default, hard or squircle.

<div class="zui-dropzone zui-dropzone-shape-hard" data-zui-dropzone style="width: 100%;">
<label class="zui-dropzone-box">
  <input class="zui-dropzone-input" type="file" />
  <i class="ph ph-upload-simple zui-dropzone-icon" aria-hidden="true"></i>
  <span class="zui-dropzone-title">Hard corners</span>
</label>
<p class="zui-dropzone-status" role="status"></p>
</div>

Checking files yourself

checkFiles is the same check the dropzone runs. Use it to check files that arrive another way, such as a paste. A server must still check every upload: the browser check is a convenience, not a guard.

import { checkFiles, formatBytes } from '@mrmartineau/zui/react'

const { accepted, rejected } = checkFiles(Array.from(files), {
  accept: 'image/*,.pdf',
  maxSize: 5 * 1024 * 1024,
})
for (const { file, reason } of rejected) {
  console.log(`${file.name}: ${reason === 'size' ? 'too big' : 'wrong type'}`)
}

Accessibility

Props

Prop Type Default Description
label string Drop files here, or click to browse Main text
hint string — Smaller text under the label, such as the allowed types and size
maxSize number — Largest file in bytes. Bigger files are refused
accept string — Allowed types: .pdf, image/*, image/png. Checked on drop too
multiple boolean false Allow more than one file
shape default | hard | squircle default Corner shape
class (className in React) string — Class for the wrapper. Other props go to the <input>

CSS custom properties

Set these on :root, on a wrapper, or on the element. See Component custom properties.

Property Default Description
--zui-dropzone-radius var(--radius-lg) Border radius of the box
--zui-dropzone-border var(--color-border) Dashed border colour
--zui-dropzone-accent var(--color-theme) Border colour on hover and while dragging
--zui-dropzone-bg transparent Background colour
--zui-dropzone-padding var(--space-md) Space inside the box
--zui-dropzone-icon-size var(--step-3) Icon size
--zui-dropzone-title-font-size var(--step-0) Label size
--zui-dropzone-hint-font-size var(--step--1) Hint and status size

Theme

Copy this CSS to your project: