Open demo

CSS code

HTML
<div><span class="lbl">Glowing dropzone with upload progress</span><div class="card">
<div class="dk" data-dk>
  <div class="dk-zone">
    <input class="dk-input" type="file" id="dk-assets" multiple accept=".zip,.dmg,.exe,.deb,.tar.gz,.gz" data-max-size="524288000" data-types="ZIP, DMG, EXE, DEB or TAR.GZ"
      aria-describedby="dk-assets-hint dk-assets-error">
    <div class="dk-face">
      <span class="dk-orb" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M21 8 12 3 3 8v8l9 5 9-5Z"/><path d="m3 8 9 5 9-5M12 13v8"/></svg></span>
      <p class="dk-title"><label for="dk-assets">Drop release builds or <b>browse</b></label></p>
      <p class="dk-hint" id="dk-assets-hint">ZIP, DMG, EXE, DEB or TAR.GZ · up to 500 MB each</p>
    </div>
  </div>
  <p class="dk-error" id="dk-assets-error" hidden></p>
  <ul class="dk-list" aria-label="Release assets" data-dk-list></ul>
  <p class="dk-sr" aria-live="polite" data-dk-live></p>
  <template><li class="dk-item" data-status="uploading"><span class="dk-type" aria-hidden="true" data-ext></span><div class="dk-body"><span class="dk-name" data-name></span><span class="dk-track"><span class="dk-fill"></span></span><span class="dk-status" data-text></span></div><button class="dk-remove" type="button" data-remove><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18M6 6l12 12"/></svg></button></li></template>
</div></div></div>

<div><span class="lbl">Dark file fields</span><div class="card">
<div class="dk">
  <div class="dk-field"><label class="dk-label" for="dk-key">Signing key</label>
    <input class="dk-file" type="file" id="dk-key" accept=".pem,.p12,.key"></div>
  <div class="dk-field"><label class="dk-label" for="dk-changelog">Changelog</label>
    <input class="dk-file glow" type="file" id="dk-changelog" accept=".md,.txt"></div>
</div></div></div>
CSS
/* Dark file uploads. A: a glowing dropzone whose files upload with a (simulated) progress bar. B: native file inputs
   restyled through ::file-selector-button for dark surfaces. Muted text is tuned to 4.5:1 on the card colour. */
.dk{--dk-accent:#8ea2ff;--dk-accent-2:#c084fc;--dk-ink:#e5e7eb;--dk-muted:#9ca3af;--dk-line:#64748b;--dk-surface:#151b2c;
  --dk-ok:#4ade80;--dk-bad:#fca5a5;display:grid;gap:14px;color:var(--dk-ink)}
.dk-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.dk-zone{position:relative}
.dk-input{position:absolute;inset:0;z-index:1;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
.dk-input::file-selector-button{cursor:pointer}
.dk-face{position:relative;display:grid;justify-items:center;gap:6px;padding:36px 24px 32px;text-align:center;background:radial-gradient(120% 90% at 50% 0%,#1a2140,#11172a 70%);
  border:1.5px dashed var(--dk-line);border-radius:16px;transition:border-color .2s,box-shadow .2s}
.dk-orb{display:grid;place-items:center;width:56px;height:56px;margin-bottom:8px;border-radius:16px;color:#0b1020;
  background:linear-gradient(135deg,var(--dk-accent),var(--dk-accent-2));box-shadow:0 10px 30px -8px rgba(142,162,255,.7);transition:transform .2s}
.dk-orb svg{width:26px;height:26px}
.dk-title{margin:0;font-size:1rem;font-weight:600}
.dk-title b{color:var(--dk-accent)}
.dk-hint{margin:0;font-size:.85rem;color:var(--dk-muted)}
.dk-input:hover + .dk-face{border-color:var(--dk-accent);box-shadow:0 0 0 4px rgba(142,162,255,.12),0 0 40px -10px rgba(142,162,255,.55)}
.dk-input:focus-visible + .dk-face{border-color:var(--dk-accent);outline:2px solid var(--dk-accent);outline-offset:4px}
.dk-zone[data-drag] .dk-face{border-style:solid;border-color:var(--dk-accent);box-shadow:0 0 0 4px rgba(142,162,255,.18),0 0 60px -8px rgba(192,132,252,.6)}
.dk-zone[data-drag] .dk-orb{transform:translateY(-4px) scale(1.06)}
.dk-error{margin:0;padding:10px 12px;font-size:.85rem;color:var(--dk-bad);background:rgba(248,113,113,.08);border:1px solid rgba(248,113,113,.35);border-radius:10px}
.dk-error[hidden]{display:none}
.dk-list{display:grid;gap:8px;margin:0;padding:0;list-style:none}
.dk-list:empty{display:none}
.dk-item{display:flex;align-items:center;gap:12px;padding:11px 10px 11px 12px;background:var(--dk-surface);border:1px solid #232c42;border-radius:12px}
.dk-type{display:grid;place-items:center;flex:none;width:38px;height:38px;border-radius:10px;font-size:.64rem;font-weight:800;letter-spacing:.04em;
  color:#c7d2fe;background:#232c4d}
.dk-body{display:grid;gap:5px;flex:1;min-width:0}
.dk-name{overflow:hidden;font-size:.9rem;font-weight:600;text-overflow:ellipsis;white-space:nowrap}
.dk-track{height:4px;border-radius:999px;background:#273049;overflow:hidden}
.dk-fill{display:block;width:calc(var(--p, 0) * 1%);height:100%;border-radius:inherit;background:linear-gradient(90deg,var(--dk-accent),var(--dk-accent-2));transition:width .2s}
[data-status="done"] .dk-fill{background:var(--dk-ok)}
.dk-status{font-size:.78rem;color:var(--dk-muted);font-variant-numeric:tabular-nums}
[data-status="done"] .dk-status{color:var(--dk-ok);font-weight:600}
.dk-remove{display:grid;place-items:center;flex:none;width:34px;height:34px;padding:0;color:#cbd5e1;background:none;border:0;border-radius:9px;cursor:pointer}
.dk-remove:hover{color:#fff;background:#232c42}
.dk-remove:focus-visible{outline:2px solid var(--dk-accent);outline-offset:1px}
.dk-remove svg{width:17px;height:17px}
/* B: dark native inputs */
.dk-field{display:grid;gap:7px}
.dk-label{font-size:.9rem;font-weight:600}
.dk-file{display:block;width:100%;margin:0;padding:6px;font:inherit;font-size:.88rem;color:var(--dk-muted);background:#0b1020;border:1px solid var(--dk-line);border-radius:12px;cursor:pointer}
.dk-file::file-selector-button{margin-right:14px;padding:8px 15px;font:inherit;font-weight:600;color:var(--dk-ink);background:#232c42;border:1px solid #334063;border-radius:8px;cursor:pointer;
  transition:background-color .15s,color .15s}
.dk-file:hover::file-selector-button{background:#2d3858}
.dk-file.glow::file-selector-button{color:#0b1020;background:linear-gradient(135deg,var(--dk-accent),var(--dk-accent-2));border:0}
.dk-file.glow:hover::file-selector-button{color:#0b1020;background:linear-gradient(135deg,#a5b4fc,#d8b4fe)}
.dk-file:focus-visible{outline:2px solid var(--dk-accent);outline-offset:3px;border-color:var(--dk-accent)}
@media (prefers-reduced-motion:reduce){.dk-face,.dk-orb,.dk-fill,.dk-file::file-selector-button{transition:none}.dk-zone[data-drag] .dk-orb{transform:none}}
JavaScript
// Dark dropzone: validates each file, lists it, and shows a simulated upload with a progress bar.
// Replace simulate() with an XMLHttpRequest and its upload.onprogress event to send real files.
document.querySelectorAll('[data-dk]').forEach(box => {
  const input = box.querySelector('input[type="file"]');
  const list = box.querySelector('[data-dk-list]');
  const tpl = box.querySelector('template');
  const live = box.querySelector('[data-dk-live]');
  const error = document.getElementById(input.id + '-error');
  const maxSize = Number(input.dataset.maxSize) || Infinity;
  const accept = input.accept.split(',').map(a => a.trim().toLowerCase()).filter(Boolean);
  const fmt = b => b < 1048576 ? Math.round(b / 1024) + ' KB' : (b / 1048576).toFixed(1).replace(/\.0$/, '') + ' MB';
  const typeOk = f => !accept.length || accept.some(a => f.name.toLowerCase().endsWith(a));

  function simulate(row) {
    const f = row.file; let loaded = 0;
    const tick = () => {
      loaded = Math.min(f.size, loaded + 380000 + Math.random() * 260000);
      const pct = Math.round(loaded / f.size * 100), done = loaded >= f.size;
      row.style.setProperty('--p', pct);
      row.dataset.status = done ? 'done' : 'uploading';
      row.querySelector('[data-text]').textContent = done ? 'Uploaded · ' + fmt(f.size) : fmt(loaded) + ' of ' + fmt(f.size) + ' · ' + pct + '%';
      if (done) { clearInterval(row.timer); live.textContent = f.name + ' uploaded.'; }
    };
    row.timer = setInterval(tick, 160); tick();
  }
  input.addEventListener('change', () => {
    const added = [], problems = [];
    for (const f of input.files) {
      if (!typeOk(f)) { problems.push(f.name + ' wasn’t added: only ' + input.dataset.types + ' files are accepted.'); continue; }
      if (f.size > maxSize) { problems.push(f.name + ' wasn’t added: it is ' + fmt(f.size) + ', the limit is ' + fmt(maxSize) + '.'); continue; }
      const row = tpl.content.firstElementChild.cloneNode(true);
      row.file = f;
      row.querySelector('[data-ext]').textContent = (f.name.split('.').pop() || 'file').slice(0, 4).toUpperCase();
      row.querySelector('[data-name]').textContent = f.name;
      const btn = row.querySelector('[data-remove]');
      btn.setAttribute('aria-label', 'Remove ' + f.name);
      btn.addEventListener('click', () => {
        const i = [...list.children].indexOf(row);
        clearInterval(row.timer); row.remove(); live.textContent = 'Removed ' + f.name + '.';
        const next = list.querySelectorAll('[data-remove]')[Math.min(i, list.children.length - 1)];
        (next || input).focus();
      });
      list.append(row); added.push(f.name); simulate(row);
    }
    input.value = '';   // the list owns the files now, so the same file can be picked again
    error.textContent = problems.join(' ');
    error.hidden = !problems.length;
    live.textContent = (added.length ? 'Uploading ' + added.join(', ') + '. ' : '') + problems.join(' ');
  });
  input.addEventListener('dragenter', () => input.parentElement.setAttribute('data-drag', ''));
  ['dragleave', 'drop'].forEach(t => input.addEventListener(t, () => input.parentElement.removeAttribute('data-drag')));
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Glowing dropzone with upload progress</span><div class="card demo-card"><div class="card-body">
<div class="dk d-grid gap-3" data-bs-theme="dark" data-dk>
  <div class="position-relative">
    <input class="dk-input position-absolute w-100 h-100 m-0 opacity-0" type="file" id="dk-assets" multiple accept=".zip,.dmg,.exe,.deb,.tar.gz,.gz" data-max-size="524288000" data-types="ZIP, DMG, EXE, DEB or TAR.GZ"
      aria-describedby="dk-assets-hint dk-assets-error">
    <div class="dk-face d-grid text-center gap-1">
      <span class="dk-orb d-grid mx-auto align-items-center justify-content-center" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M21 8 12 3 3 8v8l9 5 9-5Z"/><path d="m3 8 9 5 9-5M12 13v8"/></svg></span>
      <p class="fw-semibold mb-0"><label for="dk-assets">Drop release builds or <b>browse</b></label></p>
      <p class="small text-body-secondary mb-0" id="dk-assets-hint">ZIP, DMG, EXE, DEB or TAR.GZ · up to 500 MB each</p>
    </div>
  </div>
  <p class="alert alert-danger mb-0" id="dk-assets-error" hidden></p>
  <ul class="dk-list list-unstyled d-grid gap-2 mb-0" aria-label="Release assets" data-dk-list></ul>
  <p class="visually-hidden" aria-live="polite" data-dk-live></p>
  <template><li class="dk-item d-flex align-items-center gap-3" data-status="uploading"><span class="dk-type d-grid flex-shrink-0 rounded-3 align-items-center justify-content-center" aria-hidden="true" data-ext></span><div class="d-grid gap-1 flex-grow-1" style="min-width:0"><span class="dk-name fw-semibold text-truncate" data-name></span><div class="progress" aria-hidden="true"><div class="progress-bar"></div></div><span class="dk-status" data-text></span></div><button class="btn-close flex-shrink-0" type="button" data-remove></button></li></template>
</div></div></div></div>

<div><span class="lbl">Dark file fields</span><div class="card demo-card"><div class="card-body">
<div class="dk d-grid gap-3" data-bs-theme="dark">
  <div><label class="form-label" for="dk-key">Signing key</label>
    <input class="form-control" type="file" id="dk-key" accept=".pem,.p12,.key"></div>
  <div><label class="form-label" for="dk-changelog">Changelog</label>
    <input class="form-control glow" type="file" id="dk-changelog" accept=".md,.txt"></div>
</div></div></div></div>
CSS
/* Dark file uploads for Bootstrap 5, inside data-bs-theme="dark": .form-control[type=file], .btn-close and .alert
   pick up the dark palette; these rules add the glowing dropzone, the gradient progress and the accent button. */
.dk{--dk-accent:#8ea2ff;--dk-accent-2:#c084fc;--dk-muted:#9ca3af;--dk-line:#64748b;--dk-surface:#151b2c;--dk-ok:#4ade80;--dk-bad:#fca5a5;
  --bs-body-color:#e5e7eb;--bs-secondary-color:#9ca3af;color:#e5e7eb}
.dk-input{inset:0;z-index:1;cursor:pointer}
.dk-input::file-selector-button{cursor:pointer}
.dk-face{padding:36px 24px 32px;background:radial-gradient(120% 90% at 50% 0%,#1a2140,#11172a 70%);border:1.5px dashed var(--dk-line);border-radius:16px;
  transition:border-color .2s,box-shadow .2s}
.dk-orb{width:56px;height:56px;margin-bottom:8px;border-radius:16px;color:#0b1020;background:linear-gradient(135deg,var(--dk-accent),var(--dk-accent-2));
  box-shadow:0 10px 30px -8px rgba(142,162,255,.7);transition:transform .2s}
.dk-orb svg{width:26px;height:26px}
.dk-face b{color:var(--dk-accent)}
.dk .text-body-secondary{color:var(--dk-muted)!important}
.dk-input:hover + .dk-face{border-color:var(--dk-accent);box-shadow:0 0 0 4px rgba(142,162,255,.12),0 0 40px -10px rgba(142,162,255,.55)}
.dk-input:focus-visible + .dk-face{border-color:var(--dk-accent);outline:2px solid var(--dk-accent);outline-offset:4px}
[data-drag] > .dk-face{border-style:solid;border-color:var(--dk-accent);box-shadow:0 0 0 4px rgba(142,162,255,.18),0 0 60px -8px rgba(192,132,252,.6)}
[data-drag] .dk-orb{transform:translateY(-4px) scale(1.06)}
.dk .alert-danger{--bs-alert-color:var(--dk-bad);--bs-alert-bg:rgba(248,113,113,.08);--bs-alert-border-color:rgba(248,113,113,.35);
  --bs-alert-border-radius:10px;--bs-alert-padding-x:12px;--bs-alert-padding-y:10px;font-size:.85rem}
.dk-list:empty{display:none}
.dk-item{padding:11px 10px 11px 12px;background:var(--dk-surface);border:1px solid #232c42;border-radius:12px}
.dk-type{width:38px;height:38px;font-size:.64rem;font-weight:800;letter-spacing:.04em;color:#c7d2fe;background:#232c4d}
.dk-name{font-size:.9rem}
.dk .progress{--bs-progress-height:4px;--bs-progress-bg:#273049;--bs-progress-border-radius:999px;--bs-progress-bar-transition:width .2s ease}
.dk .progress-bar{width:calc(var(--p, 0) * 1%);background:linear-gradient(90deg,var(--dk-accent),var(--dk-accent-2))}
[data-status="done"] .progress-bar{background:var(--dk-ok)}
.dk-status{font-size:.78rem;color:var(--dk-muted);font-variant-numeric:tabular-nums}
[data-status="done"] .dk-status{color:var(--dk-ok);font-weight:600}
.dk .btn-close{--bs-btn-close-opacity:.8;--bs-btn-close-focus-shadow:0 0 0 2px var(--dk-accent);width:34px;height:34px;padding:0;background-size:11px;border-radius:9px}
.dk .btn-close:hover{background-color:#232c42}
.dk .form-label{font-size:.9rem;font-weight:600;margin-bottom:7px}
.dk .form-control[type="file"]{padding:6px;font-size:.88rem;color:var(--dk-muted);background:#0b1020;border-color:var(--dk-line);border-radius:12px;cursor:pointer}
.dk .form-control[type="file"]::file-selector-button{margin:0 14px 0 0;padding:8px 15px;font-weight:600;color:#e5e7eb;background:#232c42;border:1px solid #334063;border-radius:8px;cursor:pointer}
.dk .form-control[type="file"]:hover:not(:disabled)::file-selector-button{background:#2d3858}
.dk .form-control.glow[type="file"]::file-selector-button,.dk .form-control.glow[type="file"]:hover:not(:disabled)::file-selector-button{color:#0b1020;border:0;
  background:linear-gradient(135deg,var(--dk-accent),var(--dk-accent-2))}
.dk .form-control.glow[type="file"]:hover:not(:disabled)::file-selector-button{background:linear-gradient(135deg,#a5b4fc,#d8b4fe)}
.dk .form-control[type="file"]:focus{border-color:var(--dk-accent);box-shadow:none}
.dk .form-control[type="file"]:focus-visible{outline:2px solid var(--dk-accent);outline-offset:3px}
@media (prefers-reduced-motion:reduce){.dk-face,.dk-orb,.dk .progress-bar,.dk .form-control{transition:none}[data-drag] .dk-orb{transform:none}}
JavaScript
// Dark dropzone: validates each file, lists it, and shows a simulated upload with a progress bar.
// Replace simulate() with an XMLHttpRequest and its upload.onprogress event to send real files.
document.querySelectorAll('[data-dk]').forEach(box => {
  const input = box.querySelector('input[type="file"]');
  const list = box.querySelector('[data-dk-list]');
  const tpl = box.querySelector('template');
  const live = box.querySelector('[data-dk-live]');
  const error = document.getElementById(input.id + '-error');
  const maxSize = Number(input.dataset.maxSize) || Infinity;
  const accept = input.accept.split(',').map(a => a.trim().toLowerCase()).filter(Boolean);
  const fmt = b => b < 1048576 ? Math.round(b / 1024) + ' KB' : (b / 1048576).toFixed(1).replace(/\.0$/, '') + ' MB';
  const typeOk = f => !accept.length || accept.some(a => f.name.toLowerCase().endsWith(a));

  function simulate(row) {
    const f = row.file; let loaded = 0;
    const tick = () => {
      loaded = Math.min(f.size, loaded + 380000 + Math.random() * 260000);
      const pct = Math.round(loaded / f.size * 100), done = loaded >= f.size;
      row.style.setProperty('--p', pct);
      row.dataset.status = done ? 'done' : 'uploading';
      row.querySelector('[data-text]').textContent = done ? 'Uploaded · ' + fmt(f.size) : fmt(loaded) + ' of ' + fmt(f.size) + ' · ' + pct + '%';
      if (done) { clearInterval(row.timer); live.textContent = f.name + ' uploaded.'; }
    };
    row.timer = setInterval(tick, 160); tick();
  }
  input.addEventListener('change', () => {
    const added = [], problems = [];
    for (const f of input.files) {
      if (!typeOk(f)) { problems.push(f.name + ' wasn’t added: only ' + input.dataset.types + ' files are accepted.'); continue; }
      if (f.size > maxSize) { problems.push(f.name + ' wasn’t added: it is ' + fmt(f.size) + ', the limit is ' + fmt(maxSize) + '.'); continue; }
      const row = tpl.content.firstElementChild.cloneNode(true);
      row.file = f;
      row.querySelector('[data-ext]').textContent = (f.name.split('.').pop() || 'file').slice(0, 4).toUpperCase();
      row.querySelector('[data-name]').textContent = f.name;
      const btn = row.querySelector('[data-remove]');
      btn.setAttribute('aria-label', 'Remove ' + f.name);
      btn.addEventListener('click', () => {
        const i = [...list.children].indexOf(row);
        clearInterval(row.timer); row.remove(); live.textContent = 'Removed ' + f.name + '.';
        const next = list.querySelectorAll('[data-remove]')[Math.min(i, list.children.length - 1)];
        (next || input).focus();
      });
      list.append(row); added.push(f.name); simulate(row);
    }
    input.value = '';   // the list owns the files now, so the same file can be picked again
    error.textContent = problems.join(' ');
    error.hidden = !problems.length;
    live.textContent = (added.length ? 'Uploading ' + added.join(', ') + '. ' : '') + problems.join(' ');
  });
  input.addEventListener('dragenter', () => input.parentElement.setAttribute('data-drag', ''));
  ['dragleave', 'drop'].forEach(t => input.addEventListener(t, () => input.parentElement.removeAttribute('data-drag')));
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-dk-muted uppercase">Glowing dropzone with upload progress</span><div class="rounded-[18px] border border-[#1e2638] bg-[#0f1422] p-6 max-sm:p-[18px]">
<div class="grid gap-3.5" data-dk>
  <div class="group/zone relative">
    <input class="peer absolute inset-0 z-[1] m-0 size-full cursor-pointer opacity-0 file:cursor-pointer" type="file" id="dk-assets" multiple accept=".zip,.dmg,.exe,.deb,.tar.gz,.gz" data-max-size="524288000" data-types="ZIP, DMG, EXE, DEB or TAR.GZ"
      aria-describedby="dk-assets-hint dk-assets-error">
    <div class="grid justify-items-center gap-1.5 rounded-2xl border-[1.5px] border-dashed border-dk-line bg-[radial-gradient(120%_90%_at_50%_0%,#1a2140,#11172a_70%)] px-6 pt-9 pb-8 text-center transition-[border-color,box-shadow] duration-200 peer-hover:border-dk-accent peer-hover:shadow-[0_0_0_4px_rgba(142,162,255,.12),0_0_40px_-10px_rgba(142,162,255,.55)] peer-focus-visible:border-dk-accent peer-focus-visible:outline-2 peer-focus-visible:outline-offset-4 peer-focus-visible:outline-dk-accent group-data-[drag]/zone:border-solid group-data-[drag]/zone:border-dk-accent group-data-[drag]/zone:shadow-[0_0_0_4px_rgba(142,162,255,.18),0_0_60px_-8px_rgba(192,132,252,.6)] motion-reduce:transition-none">
      <span class="mb-2 grid size-14 place-items-center rounded-2xl bg-linear-135 from-dk-accent to-dk-accent-2 text-[#0b1020] shadow-[0_10px_30px_-8px_rgba(142,162,255,.7)] transition-transform duration-200 group-data-[drag]/zone:-translate-y-1 group-data-[drag]/zone:scale-106 motion-reduce:transition-none motion-reduce:group-data-[drag]/zone:transform-none" aria-hidden="true"><svg class="size-[26px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M21 8 12 3 3 8v8l9 5 9-5Z"/><path d="m3 8 9 5 9-5M12 13v8"/></svg></span>
      <p class="font-semibold"><label for="dk-assets">Drop release builds or <b class="text-dk-accent">browse</b></label></p>
      <p class="text-[.85rem] text-dk-muted" id="dk-assets-hint">ZIP, DMG, EXE, DEB or TAR.GZ · up to 500 MB each</p>
    </div>
  </div>
  <p class="rounded-[10px] border border-red-400/35 bg-red-400/8 px-3 py-2.5 text-[.85rem] text-dk-bad" id="dk-assets-error" hidden></p>
  <ul class="grid gap-2 empty:hidden" aria-label="Release assets" data-dk-list></ul>
  <p class="sr-only" aria-live="polite" data-dk-live></p>
  <template><li class="group flex items-center gap-3 rounded-xl border border-[#232c42] bg-dk-surface py-[11px] pr-2.5 pl-3" data-status="uploading"><span class="grid size-[38px] flex-none place-items-center rounded-[10px] bg-[#232c4d] text-[.64rem] font-extrabold tracking-[.04em] text-indigo-200" aria-hidden="true" data-ext></span><div class="grid min-w-0 flex-1 gap-[5px]"><span class="truncate text-[.9rem] font-semibold" data-name></span><span class="h-1 overflow-hidden rounded-full bg-[#273049]"><span class="block h-full w-[calc(var(--p,0)*1%)] rounded-[inherit] bg-linear-90 from-dk-accent to-dk-accent-2 transition-[width] duration-200 group-data-[status=done]:bg-dk-ok group-data-[status=done]:bg-none motion-reduce:transition-none"></span></span><span class="text-[.78rem] text-dk-muted tabular-nums group-data-[status=done]:font-semibold group-data-[status=done]:text-dk-ok" data-text></span></div><button class="grid size-[34px] flex-none cursor-pointer place-items-center rounded-[9px] text-slate-300 hover:bg-[#232c42] hover:text-white focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dk-accent" type="button" data-remove><svg class="size-[17px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18M6 6l12 12"/></svg></button></li></template>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-dk-muted uppercase">Dark file fields</span><div class="rounded-[18px] border border-[#1e2638] bg-[#0f1422] p-6 max-sm:p-[18px]">
<div class="grid gap-3.5">
  <div class="grid gap-[7px]"><label class="text-[.9rem] font-semibold" for="dk-key">Signing key</label>
    <input class="block w-full cursor-pointer rounded-xl border border-dk-line bg-[#0b1020] p-1.5 text-[.88rem] text-dk-muted focus-visible:border-dk-accent focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-dk-accent file:mr-3.5 file:cursor-pointer file:rounded-lg file:border file:border-[#334063] file:bg-[#232c42] file:px-[15px] file:py-2 file:font-semibold file:text-gray-200 file:transition-colors hover:file:bg-[#2d3858] motion-reduce:file:transition-none" type="file" id="dk-key" accept=".pem,.p12,.key"></div>
  <div class="grid gap-[7px]"><label class="text-[.9rem] font-semibold" for="dk-changelog">Changelog</label>
    <input class="block w-full cursor-pointer rounded-xl border border-dk-line bg-[#0b1020] p-1.5 text-[.88rem] text-dk-muted focus-visible:border-dk-accent focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-dk-accent file:mr-3.5 file:cursor-pointer file:rounded-lg file:border-0 file:bg-linear-135 file:from-dk-accent file:to-dk-accent-2 file:px-[15px] file:py-2 file:font-semibold file:text-[#0b1020] hover:file:from-indigo-300 hover:file:to-purple-300" type="file" id="dk-changelog" accept=".md,.txt"></div>
</div></div></div>
CSS
@theme {
  --color-dk-accent: #8ea2ff;
  --color-dk-accent-2: #c084fc;
  --color-dk-muted: #9ca3af;
  --color-dk-line: #64748b;
  --color-dk-surface: #151b2c;
  --color-dk-ok: #4ade80;
  --color-dk-bad: #fca5a5;
}
JavaScript
// Dark dropzone: validates each file, lists it, and shows a simulated upload with a progress bar.
// Replace simulate() with an XMLHttpRequest and its upload.onprogress event to send real files.
document.querySelectorAll('[data-dk]').forEach(box => {
  const input = box.querySelector('input[type="file"]');
  const list = box.querySelector('[data-dk-list]');
  const tpl = box.querySelector('template');
  const live = box.querySelector('[data-dk-live]');
  const error = document.getElementById(input.id + '-error');
  const maxSize = Number(input.dataset.maxSize) || Infinity;
  const accept = input.accept.split(',').map(a => a.trim().toLowerCase()).filter(Boolean);
  const fmt = b => b < 1048576 ? Math.round(b / 1024) + ' KB' : (b / 1048576).toFixed(1).replace(/\.0$/, '') + ' MB';
  const typeOk = f => !accept.length || accept.some(a => f.name.toLowerCase().endsWith(a));

  function simulate(row) {
    const f = row.file; let loaded = 0;
    const tick = () => {
      loaded = Math.min(f.size, loaded + 380000 + Math.random() * 260000);
      const pct = Math.round(loaded / f.size * 100), done = loaded >= f.size;
      row.style.setProperty('--p', pct);
      row.dataset.status = done ? 'done' : 'uploading';
      row.querySelector('[data-text]').textContent = done ? 'Uploaded · ' + fmt(f.size) : fmt(loaded) + ' of ' + fmt(f.size) + ' · ' + pct + '%';
      if (done) { clearInterval(row.timer); live.textContent = f.name + ' uploaded.'; }
    };
    row.timer = setInterval(tick, 160); tick();
  }
  input.addEventListener('change', () => {
    const added = [], problems = [];
    for (const f of input.files) {
      if (!typeOk(f)) { problems.push(f.name + ' wasn’t added: only ' + input.dataset.types + ' files are accepted.'); continue; }
      if (f.size > maxSize) { problems.push(f.name + ' wasn’t added: it is ' + fmt(f.size) + ', the limit is ' + fmt(maxSize) + '.'); continue; }
      const row = tpl.content.firstElementChild.cloneNode(true);
      row.file = f;
      row.querySelector('[data-ext]').textContent = (f.name.split('.').pop() || 'file').slice(0, 4).toUpperCase();
      row.querySelector('[data-name]').textContent = f.name;
      const btn = row.querySelector('[data-remove]');
      btn.setAttribute('aria-label', 'Remove ' + f.name);
      btn.addEventListener('click', () => {
        const i = [...list.children].indexOf(row);
        clearInterval(row.timer); row.remove(); live.textContent = 'Removed ' + f.name + '.';
        const next = list.querySelectorAll('[data-remove]')[Math.min(i, list.children.length - 1)];
        (next || input).focus();
      });
      list.append(row); added.push(f.name); simulate(row);
    }
    input.value = '';   // the list owns the files now, so the same file can be picked again
    error.textContent = problems.join(' ');
    error.hidden = !problems.length;
    live.textContent = (added.length ? 'Uploading ' + added.join(', ') + '. ' : '') + problems.join(' ');
  });
  input.addEventListener('dragenter', () => input.parentElement.setAttribute('data-drag', ''));
  ['dragleave', 'drop'].forEach(t => input.addEventListener(t, () => input.parentElement.removeAttribute('data-drag')));
});

About this set

File upload components for dark interfaces such as developer tools, dashboards and media apps. Glowing dropzone with upload progress is a release asset uploader: a dashed zone on a deep blue radial gradient with a gradient icon tile, a soft indigo glow on hover and a stronger violet glow while a file is dragged over it. Accepted builds appear as rows with a type badge, the file name, a thin gradient progress bar and a readout of the bytes sent, then turn green with Uploaded and the size when they finish. The upload is simulated so the states can be seen, and the row list owns the files so the same file can be added twice. Dark file fields restyles two native inputs for dark forms, one with a quiet slate button and one with the indigo to violet gradient, through ::file-selector-button. The palette is tuned for contrast rather than mood alone: muted text reaches 4.5 to 1 on the card colour, the dashed border reaches 3 to 1, and the focus ring uses the bright accent. The Bootstrap version sets data-bs-theme to dark, so Bootstrap’s own fields, alerts and close buttons follow.

What’s included

  • Glowing dropzone with hover and dragover states
  • Gradient progress bars that turn green when done
  • Type and size checks with messages
  • Dark native inputs styled through ::file-selector-button
  • Muted text and borders checked for contrast on dark

Accessibility

The dropzone input is labelled by its visible text and described by the limits and the error message, with a bright outline on the zone when it has keyboard focus. Remove buttons are named after their file, finished uploads and rejections are announced in a polite live region, and progress is written out as text as well as drawn. Glow, bar and hover transitions switch off under prefers-reduced-motion.

Customise it

Change --dk-accent and --dk-accent-2 for the glow and gradient, --dk-line and --dk-muted for borders and secondary text, on .dk. Bootstrap adds data-bs-theme="dark"; Tailwind reads the dk colours from @theme.