GitHub

Components / Form & Input / File Upload

File Upload beta

A dropzone that says what it accepts, and a list that shows what it got.

Preview
  • q3-board-deck.pdf2.4 MB
<div class="k-up" data-upload style="max-width:430px">
  <label class="k-up-zone">
    <input type="file" multiple hidden>
    <span class="ic"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15V3M7 8l5-5 5 5"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg></span>
    <b>Drop files here, or <u>browse</u></b>
    <span>PNG, JPG or PDF — up to 10 MB each</span>
  </label>
  <ul class="k-up-list" data-up-list data-icon="<svg width=&quot;17&quot; height=&quot;17&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;><path d=&quot;M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z&quot;/><path d=&quot;M14 3v5h5&quot;/></svg>">
    <li class="k-up-item"><span class="fi"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z"/><path d="M14 3v5h5"/></svg></span><span class="nm">q3-board-deck.pdf</span><span class="sz">2.4 MB</span><button class="rm" type="button" aria-label="Remove q3-board-deck.pdf"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button></li>
  </ul>
</div>
.k-up{font-family:var(--k-font);text-align:left}
.k-up-zone{display:grid;justify-items:center;gap:7px;border:1.5px dashed var(--k-border-strong);border-radius:14px;padding:26px 18px;text-align:center;cursor:pointer;color:var(--k-ink-2);transition:border-color .15s,background .15s}
.k-up-zone:hover{border-color:var(--k-accent);background:var(--k-accent-bg)}
.k-up-zone.drag{border-color:var(--k-accent);background:var(--k-accent-bg)}
.k-up-zone .ic{color:var(--k-ink-3);display:grid;place-items:center}
.k-up-zone b{font-size:13.5px;font-weight:600;color:var(--k-ink)}
.k-up-zone b u{text-decoration-color:var(--k-accent);text-underline-offset:3px}
.k-up-zone span{font-size:12px;color:var(--k-ink-3)}
.k-up-list{list-style:none;margin:12px 0 0;padding:0;display:grid;gap:8px}
.k-up-item{display:flex;align-items:center;gap:11px;border:1px solid var(--k-line);border-radius:11px;padding:9px 12px;background:var(--k-bg)}
.k-up-item .fi{color:var(--k-ink-3);display:grid;place-items:center;flex:none}
.k-up-item .nm{font-size:13px;font-weight:550;color:var(--k-ink);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.k-up-item .sz{font-size:11.5px;color:var(--k-ink-3);font-family:var(--k-mono)}
.k-up-item .rm{width:26px;height:26px;border:none;background:transparent;color:var(--k-ink-3);border-radius:7px;display:grid;place-items:center;cursor:pointer;flex:none}
.k-up-item .rm:hover{background:var(--k-bg-soft);color:var(--k-danger)}
const up = document.querySelector('[data-upload]');
const upInput = up.querySelector('input[type="file"]');
const upZone = up.querySelector('.k-up-zone');
const upList = up.querySelector('[data-up-list]');
const FILE_SVG = upList.dataset.icon;
function upSize(n) {
  return n > 1048576 ? (n / 1048576).toFixed(1) + ' MB' : Math.max(1, Math.round(n / 1024)) + ' KB';
}
function upAdd(files) {
  [...files].forEach(f => {
    const li = document.createElement('li');
    li.className = 'k-up-item';
    li.innerHTML = `<span class="fi">${FILE_SVG}</span><span class="nm"></span><span class="sz">${upSize(f.size)}</span><button class="rm" type="button" aria-label="Remove file">✕</button>`;
    li.querySelector('.nm').textContent = f.name;
    li.querySelector('.rm').addEventListener('click', () => li.remove());
    upList.appendChild(li);
  });
}
upInput.addEventListener('change', () => { upAdd(upInput.files); upInput.value = ''; });
['dragenter', 'dragover'].forEach(ev => upZone.addEventListener(ev, e => { e.preventDefault(); upZone.classList.add('drag'); }));
['dragleave', 'drop'].forEach(ev => upZone.addEventListener(ev, e => { e.preventDefault(); upZone.classList.remove('drag'); }));
upZone.addEventListener('drop', e => upAdd(e.dataTransfer.files));
upList.querySelectorAll('.rm').forEach(b => b.addEventListener('click', () => b.closest('.k-up-item').remove()));

Usage

For attachments and imports. State the accepted types and the size limit on the zone itself — not in a help article the user will never open.

Accessibility

The zone is a real label for a real file input, so keyboard and screen-reader users browse the same way. Dropped files join the same list as browsed ones, each removable.

Related parts