SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/file-upload-buttons

9 CSS File Upload Buttons

1 Pure CSS8 with JSPublished 2026-10-04Free · no sign-up

Nine file upload patterns built on the native file input: a custom Choose file button, a drag and drop zone, a file list with remove buttons, an upload with a progress bar, image previews, a chat attach button, a round photo picker, type and size validation and the simplest styled native input.

01. Styled File Upload Button

UploadForm

A custom Choose file button built on a real file input. The input is visually hidden but focusable, the label is styled as the button, and the chosen file name appears next to it, so the native behaviour stays intact.

View code
<div class="fub">
  <input type="file" id="fub-i">
  <label for="fub-i">Choose file</label>
  <span id="fub-n">No file chosen</span>
</div>
.fub { box-sizing: border-box; max-width: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 12px; font: 600 14px Inter, system-ui, sans-serif; color: #475467; }
.fub input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.fub label { padding: 10px 16px; border-radius: 10px; background: #2563eb; color: #fff; cursor: pointer; }
.fub label:hover { background: #1d4ed8; }
.fub input:focus-visible + label { outline: 2px solid #1e3a8a; outline-offset: 2px; }
document.getElementById('fub-i').addEventListener('change', e => {
  document.getElementById('fub-n').textContent = e.target.files[0] ? e.target.files[0].name : 'No file chosen';
});

02. Drag and Drop Zone

Drag and dropUpload

A drop zone that highlights when a file is dragged over it and also works as a click target for people who cannot drag. Dropped and chosen files share one handler, and the zone states the accepted types and size limit in text.

View code
<label class="dz" id="dz">
  <input type="file" multiple accept=".jpg,.png,.pdf">
  <b>Drop files here or <u>browse</u></b>
  <small>JPG, PNG or PDF, up to 10 MB each</small>
  <span class="out" aria-live="polite"></span>
</label>
.dz { box-sizing: border-box; max-width: 100%; width: 360px; display: grid; justify-items: center; gap: 6px; padding: 34px 20px; border: 2px dashed #3a4256; border-radius: 16px; background: #12161f; color: #c9d1e2; font: 14px Inter, system-ui, sans-serif; text-align: center; cursor: pointer; transition: border-color .2s, background .2s; }
.dz input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.dz b { color: #fff; font-size: 15px; }
.dz u { color: #7c9cff; }
.dz small { color: #9aa4b8; }
.dz.over { border-color: #7c9cff; background: #172036; }
.dz:focus-within { border-color: #7c9cff; }
.out { color: #34d399; font-weight: 600; }
const dz = document.getElementById('dz'), inp = dz.querySelector('input'), out = dz.querySelector('.out');
function show(files) { out.textContent = files.length ? files.length + ' file(s) ready: ' + [...files].map(f => f.name).join(', ') : ''; }
['dragenter', 'dragover'].forEach(ev => dz.addEventListener(ev, e => { e.preventDefault(); dz.classList.add('over'); }));
['dragleave', 'drop'].forEach(ev => dz.addEventListener(ev, e => { e.preventDefault(); dz.classList.remove('over'); }));
dz.addEventListener('drop', e => show(e.dataTransfer.files));
inp.addEventListener('change', () => show(inp.files));

03. Upload with File List

UploadList

A file picker that lists every selected file with its size and a remove button. Sizes are formatted in KB or MB, each remove button is named after its file, and the list is rebuilt from a DataTransfer so the input matches what is shown.

View code
<div class="ful">
  <label class="pick">Add files<input type="file" multiple id="ful-i"></label>
  <ul id="ful-l"><li class="empty">No files yet</li></ul>
</div>
.ful { box-sizing: border-box; max-width: 100%; width: 360px; font-family: Inter, system-ui, sans-serif; }
.pick { display: inline-block; padding: 9px 14px; border: 1.5px solid #d0d5dd; border-radius: 10px; background: #fff; color: #101828; font-weight: 700; font-size: 14px; cursor: pointer; }
.pick input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.pick:focus-within { outline: 2px solid #2563eb; outline-offset: 2px; }
.ful ul { display: grid; gap: 6px; margin: 12px 0 0; padding: 0; list-style: none; }
.ful li { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid #e4e7ec; border-radius: 10px; background: #fff; font-size: 14px; color: #101828; }
.ful li.empty { color: #5d6679; border-style: dashed; }
.ful li span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ful small { color: #5d6679; }
.ful li button { border: 0; background: none; color: #b42318; font: 700 13px Inter, sans-serif; cursor: pointer; }
.ful li button:focus-visible { outline: 2px solid #b42318; }
const fi = document.getElementById('ful-i'), fl = document.getElementById('ful-l');
const size = b => b > 1048576 ? (b / 1048576).toFixed(1) + ' MB' : Math.max(1, Math.round(b / 1024)) + ' KB';
function draw() {
  fl.innerHTML = '';
  if (!fi.files.length) { fl.innerHTML = '<li class="empty">No files yet</li>'; return; }
  [...fi.files].forEach((f, i) => {
    const li = document.createElement('li');
    li.innerHTML = '<span></span><small>' + size(f.size) + '</small><button type="button">Remove</button>';
    li.querySelector('span').textContent = f.name;
    li.querySelector('button').setAttribute('aria-label', 'Remove ' + f.name);
    li.querySelector('button').addEventListener('click', () => {
      const dt = new DataTransfer(); [...fi.files].forEach((g, k) => { if (k !== i) dt.items.add(g); });
      fi.files = dt.files; draw(); fi.focus();
    });
    fl.appendChild(li);
  });
}
fi.addEventListener('change', draw);

04. Upload with Progress Bar

UploadProgress

A file row that shows upload progress after a file is chosen, with a simulated transfer in this demo. The progress element announces its value, a cancel button stops the transfer, and the row turns green with a done state at the end.

View code
<div class="fup">
  <label class="pick">Upload a file<input type="file" id="fup-i"></label>
  <div class="row" id="fup-r" hidden>
    <span id="fup-n"></span>
    <progress id="fup-p" max="100" value="0" aria-label="Upload progress"></progress>
    <button type="button" id="fup-c">Cancel</button>
  </div>
</div>
.fup { box-sizing: border-box; max-width: 100%; width: 360px; display: grid; gap: 12px; justify-items: start; font: 14px Inter, system-ui, sans-serif; color: #c9d1e2; }
.pick { padding: 9px 14px; border-radius: 10px; background: #7c9cff; color: #0e1118; font-weight: 700; cursor: pointer; }
.pick input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.pick:focus-within { outline: 2px solid #fff; outline-offset: 2px; }
.row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 12px; width: 100%; box-sizing: border-box; padding: 12px; border: 1px solid #262d3e; border-radius: 12px; background: #12161f; }
.row span { overflow: hidden; color: #fff; text-overflow: ellipsis; white-space: nowrap; }
.row progress { grid-column: 1 / -1; grid-row: 2; width: 100%; height: 8px; accent-color: #7c9cff; }
.row button { border: 0; background: none; color: #fca5a5; font: 700 13px Inter, sans-serif; cursor: pointer; }
.row.ok { border-color: #34d399; }
const fi = document.getElementById('fup-i'), row = document.getElementById('fup-r'), p = document.getElementById('fup-p'), c = document.getElementById('fup-c');
let t;
fi.addEventListener('change', () => {
  if (!fi.files[0]) return;
  clearInterval(t); row.hidden = false; row.classList.remove('ok'); p.value = 0; c.hidden = false;
  document.getElementById('fup-n').textContent = fi.files[0].name;
  t = setInterval(() => { p.value += 10; if (p.value >= 100) { clearInterval(t); row.classList.add('ok'); c.hidden = true; document.getElementById('fup-n').textContent += ' uploaded'; } }, 200);
});
c.addEventListener('click', () => { clearInterval(t); row.hidden = true; fi.value = ''; fi.focus(); });

05. Image Upload with Preview

ImagePreview

An image field that shows a thumbnail grid of chosen pictures before upload, useful for product photos. Previews use object URLs so nothing leaves the device, and each thumbnail's alt text is the file name for a basic description.

View code
<div class="fui">
  <label class="tile"><input type="file" accept="image/*" multiple id="fui-i"><span aria-hidden="true">+</span><b>Add photos</b></label>
  <div id="fui-g" class="grid"></div>
</div>
.fui { box-sizing: border-box; max-width: 100%; width: 360px; display: flex; flex-wrap: wrap; gap: 10px; font-family: Inter, system-ui, sans-serif; }
.tile { display: grid; place-content: center; justify-items: center; width: 96px; height: 96px; border: 2px dashed #98a2b3; border-radius: 14px; color: #475467; cursor: pointer; }
.tile input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.tile span { font-size: 26px; line-height: 1; }
.tile b { font-size: 12px; }
.tile:focus-within { border-color: #2563eb; color: #1d4ed8; }
.grid { display: contents; }
.grid img { width: 96px; height: 96px; border-radius: 14px; object-fit: cover; }
document.getElementById('fui-i').addEventListener('change', e => {
  const g = document.getElementById('fui-g'); g.innerHTML = '';
  [...e.target.files].forEach(f => { const im = new Image(); im.alt = f.name; im.src = URL.createObjectURL(f); g.appendChild(im); });
});

06. Compact Attach Button

ChatIcon

A paperclip attach button for chat and email composers. It is an icon label for a hidden file input, with an accessible name of Attach a file, and it shows a small count badge once files are attached.

View code
<div class="cmp">
  <input placeholder="Write a message" aria-label="Message">
  <label class="att"><input type="file" multiple id="att-i"><span class="sr">Attach a file</span><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 11l-8.5 8.5a5 5 0 01-7-7L14 4a3.5 3.5 0 015 5l-8.5 8.5a2 2 0 01-3-3L15 7"/></svg><i id="att-c" hidden></i></label>
</div>
.cmp { box-sizing: border-box; max-width: 100%; width: 360px; display: flex; align-items: center; gap: 6px; padding: 6px 6px 6px 14px; border: 1px solid #343c51; border-radius: 999px; background: #151a26; font-family: Inter, system-ui, sans-serif; }
.cmp > input { flex: 1; min-width: 0; border: 0; background: none; color: #fff; font-size: 15px; outline: none; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.att { position: relative; display: grid; place-items: center; width: 38px; height: 38px; border-radius: 50%; cursor: pointer; }
.att:hover { background: #1e2433; }
.att input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.att:focus-within { outline: 2px solid #7c9cff; }
.att svg { width: 20px; height: 20px; fill: none; stroke: #c9d1e2; stroke-width: 2; stroke-linecap: round; }
.att i { position: absolute; top: 0; right: 0; min-width: 16px; height: 16px; border-radius: 999px; background: #7c9cff; color: #0e1118; font: 700 10px/16px Inter, sans-serif; font-style: normal; text-align: center; }
document.getElementById('att-i').addEventListener('change', e => {
  const c = document.getElementById('att-c'), n = e.target.files.length;
  c.hidden = !n; c.textContent = n;
});

07. Circular Photo Picker

ProfileUpload

A circular picker for profile photos with a camera icon in the middle and a hover overlay that says Change. Once a picture is chosen it fills the circle; the label text stays available to screen readers in every state.

View code
<label class="cpk">
  <input type="file" accept="image/*" id="cpk-i">
  <img id="cpk-img" alt="" hidden>
  <span class="ov"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8h3l2-3h6l2 3h3v11H4z"/><circle cx="12" cy="13" r="3.5"/></svg>Upload photo</span>
</label>
.cpk { box-sizing: border-box; max-width: 100%; position: relative; display: block; width: 120px; height: 120px; overflow: hidden; border: 2px dashed #98a2b3; border-radius: 50%; background: #f9fafb; cursor: pointer; font: 600 12px Inter, system-ui, sans-serif; }
.cpk input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.cpk img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.ov { position: absolute; inset: 0; display: grid; place-content: center; justify-items: center; gap: 4px; color: #475467; }
.ov svg { width: 26px; height: 26px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linejoin: round; }
.cpk.has .ov { background: rgba(16,24,40,.55); color: #fff; opacity: 0; transition: opacity .2s; }
.cpk.has:hover .ov, .cpk.has:focus-within .ov { opacity: 1; }
.cpk:focus-within { border-color: #2563eb; border-style: solid; }
document.getElementById('cpk-i').addEventListener('change', e => {
  const f = e.target.files[0]; if (!f) return;
  const img = document.getElementById('cpk-img'); img.src = URL.createObjectURL(f); img.hidden = false;
  const l = document.querySelector('.cpk'); l.classList.add('has'); l.querySelector('.ov').lastChild.textContent = 'Change photo';
});

08. Upload with Type and Size Check

ValidationUpload

A CV upload that checks the file type and size before accepting it and shows a clear error otherwise. The accept attribute filters the picker, but the script still checks, because accept is only a hint; errors use role="alert".

View code
<div class="fuv">
  <label for="fuv-i">Upload your CV <small>(PDF or DOCX, max 2 MB)</small></label>
  <input type="file" id="fuv-i" accept=".pdf,.docx" aria-describedby="fuv-m">
  <p id="fuv-m"></p>
</div>
.fuv { box-sizing: border-box; max-width: 100%; width: 340px; display: grid; gap: 8px; font: 14px Inter, system-ui, sans-serif; color: #c9d1e2; }
.fuv label { color: #fff; font-weight: 600; }
.fuv small { color: #9aa4b8; font-weight: 400; }
.fuv input { color: #c9d1e2; font: 14px Inter, sans-serif; }
.fuv input::file-selector-button { margin-right: 12px; padding: 8px 14px; border: 0; border-radius: 8px; background: #fbbf24; color: #1a1205; font: 700 13px Inter, sans-serif; cursor: pointer; }
.fuv input:focus-visible { outline: 2px solid #fbbf24; outline-offset: 2px; }
#fuv-m { min-height: 20px; margin: 0; }
#fuv-m.err { color: #fca5a5; }
#fuv-m.ok { color: #6ee7b7; }
const fv = document.getElementById('fuv-i'), fm = document.getElementById('fuv-m');
fv.addEventListener('change', () => {
  const f = fv.files[0]; if (!f) return;
  const okType = /\.(pdf|docx)$/i.test(f.name), okSize = f.size <= 2 * 1024 * 1024;
  if (okType && okSize) { fm.className = 'ok'; fm.removeAttribute('role'); fm.textContent = f.name + ' is ready to send.'; }
  else { fm.className = 'err'; fm.setAttribute('role', 'alert'); fm.textContent = !okType ? 'That file type is not allowed. Choose a PDF or DOCX.' : 'That file is larger than 2 MB.'; fv.value = ''; }
});

09. Native Input with file-selector-button

NativePure CSS

The simplest custom upload: keep the native file input and restyle only its button with the ::file-selector-button pseudo-element. You get full browser behaviour, the file name display and keyboard support with a few lines of CSS.

View code
<label class="fsn">Attach invoice <input type="file"></label>
.fsn { box-sizing: border-box; max-width: 100%; display: grid; gap: 8px; color: #344054; font: 600 14px Inter, system-ui, sans-serif; }
.fsn input { max-width: 100%; color: #475467; font: 14px Inter, sans-serif; }
.fsn input::file-selector-button { margin-right: 12px; padding: 9px 14px; border: 1.5px solid #d0d5dd; border-radius: 10px; background: #fff; color: #101828; font: 700 13.5px Inter, sans-serif; cursor: pointer; transition: background .15s; }
.fsn input::file-selector-button:hover { background: #f2f4f7; }
.fsn input:focus-visible { outline: 2px solid #2563eb; outline-offset: 3px; border-radius: 10px; }

About these CSS File Upload Buttons

Keep the real input

Every design here keeps <input type="file"> in the page, visually hidden but focusable, and styles its label instead. That preserves the file picker, keyboard access and mobile camera options.

The quickest restyle

::file-selector-button styles the native button directly, so a few lines of CSS give a branded upload control with no hidden inputs at all.

Drag and drop is extra

A drop zone should also be clickable and keyboard reachable, because many people cannot drag. Handle dropped files and chosen files with the same function.

Check files yourself

The accept attribute only filters the picker. Check type and size in your script and again on the server, and show clear errors with role="alert".

Frequently asked questions

Are files uploaded anywhere in these demos?

No. Previews use object URLs on your own device, and the progress demo simulates the transfer.

Can I allow several files?

Yes. Add the multiple attribute and read input.files as a list.

How do I remove one file from the selection?

Rebuild the list with a DataTransfer object, as the file list demo does.