Components / Form & Input / File Upload
File Upload beta
A dropzone that says what it accepts, and a list that shows what it got.
- 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="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>">
<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
Button stable
Black leads; lines and ghosts support. Hover, disabled and loading states included.
Checkbox / Radio stable
Native inputs with a restyled face. Keyboard support and form behavior stay intact.
Color Picker beta
A curated palette first, any color second — swatches for the 95% case, a native picker for the rest.
Combobox beta
Type to filter, arrows to choose — the Select for lists too long to scan.