v1.2.0
  1. ds
  2. Components
  3. Drop zone

Drop zone

Drop a file, pick one, or paste. Nothing is uploaded.

Code
<label class="drop">
  <svg class="i" viewBox="0 0 24 24" aria-hidden="true">…</svg>
  <strong>Drop a crontab here</strong>
  <span>or <span class="pick">choose a file</span></span>
  <span class="hint">You can also paste it with <kbd class="kbd">Ctrl</kbd> <kbd class="kbd">V</kbd></span>
  <input type="file" accept=".txt,.cron,text/plain" />
</label>

The whole zone is the label of a real file input, so a click anywhere opens the picker and the input stays in the tab order. Try it: drop a file, pick one, or paste text anywhere on this page. It only reads the name and size, and nothing leaves the browser.

States

Release to read itNothing is uploaded
Drop a crontab hereThat file is 4.2 MB. The limit is 1 MB.
Code
<div class="drop over">…</div>                      <!-- a file is being dragged over -->
<div class="drop" aria-invalid="true">…<span class="err">That file is 4.2 MB. The limit is 1 MB.</span></div>

Add .over (or data-over) on dragenter and remove it on dragleave and drop: the border and the wash turn purple. Keyboard focus on the input rings the whole zone.

Compact, and a file that has been read

orders.schema.json2.1 KB · 64 lines
Code
<label class="drop sm">
  <svg class="i">…</svg>
  <span><strong>Drop a schema</strong> or <span class="pick">choose a file</span></span>
  <span class="hint">JSON or YAML</span>
  <input type="file" />
</label>
<div class="drop-file">
  <svg class="i">…</svg><b>orders.schema.json</b><small>2.1 KB · 64 lines</small>
  <button class="btn xs ghost" type="button">Remove</button>
</div>

.sm is one line, for a toolbar or the top of a pane. Once a file is in, replace the zone with a .drop-file row, or keep the zone and put the row under it.

Wiring

const read = (file) => file.text().then(load);
zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('over'); });
zone.addEventListener('dragleave', () => zone.classList.remove('over'));
zone.addEventListener('drop', (e) => {
  e.preventDefault();
  zone.classList.remove('over');
  if (e.dataTransfer.files[0]) read(e.dataTransfer.files[0]);
});
zone.querySelector('input').addEventListener('change', (e) => e.target.files[0] && read(e.target.files[0]));
addEventListener('paste', (e) => {
  if (e.target.closest('input, textarea')) return;
  load(e.clipboardData.getData('text'));
});

API

ClassOnWhat it does
.droplabel, divDashed zone on --bg. Put an icon, a strong line, text and a .hint inside.
.pickspanThe "choose a file" words, styled as a link.
.over / data-over.dropSomething is being dragged over it.
aria-invalid="true".dropRed border. Say why in an .err line.
.sm.dropOne line, for toolbars and pane heads.
.drop-filedivA file that has been read: icon, name in <b>, size in <small>, an action.

Accessibility

  • Keep the real input type="file" inside the zone. ds.css hides it visually without removing it, so it keeps its keyboard focus and its name from the label.
  • Dragging is a shortcut, not the only way: the picker and paste do the same job.
  • Announce the result in a role="status" region: "orders.schema.json, 64 lines".