Component

Upload Progress

Multi-file upload list where each file shows an animated progress bar and percentage, then transitions to a green success check when complete. Files can be removed mid-upload. Call addFile(name) to push files programmatically.

  • Component
  • Tailwind
  • HTML
  • MIT

Preview

Live component

Interactive preview
Open preview
9:41 100%
devsnips.dev/library/tailwind/components/progress/upload-progress/
fluid · no fixed width 100%
No dependencies Focus-visible

Install

Add to your project

terminal
npx devsnips add Tailwind/Components/Progress/upload-progress
registry path: Tailwind/Components/Progress/upload-progress

Source

Implementation

code.html
<!--
Snippet Name: Upload Progress
Description: Multi-file upload list where each file shows an animated progress bar, then transitions to a success check. Files can be removed before completing.
Author: DevSnips Contributors
Usage Example: Call addFile(name) to push a file; the list simulates upload progress and flips to success.
-->
<div class="w-full max-w-md space-y-3" data-upload-list></div>

<!-- Demo trigger -->
<label class="inline-flex cursor-pointer items-center gap-2 rounded-xl bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-blue-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2">
  <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3 3-3"/></svg>
  Upload files
  <input type="file" multiple class="sr-only" />
</label>

<script>
  (function () {
    const list = document.currentScript.closest('[data-upload-list]') || document.querySelector('[data-upload-list]');
    if (!list) return;

    function el(tag, cls, html) {
      const e = document.createElement(tag);
      if (cls) e.className = cls;
      if (html !== undefined) e.innerHTML = html;
      return e;
    }

    function addFile(name) {
      const item = el('div', 'upload-item flex items-center gap-3 rounded-xl border border-gray-200 bg-white p-3 shadow-sm');
      item.innerHTML = `
        <span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-blue-50 text-blue-600">
          <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
        </span>
        <div class="min-w-0 flex-1">
          <div class="mb-1.5 flex items-center justify-between gap-2">
            <p class="upload-name truncate text-sm font-medium text-gray-900"></p>
            <span class="upload-pct text-xs font-semibold tabular-nums text-gray-500">0%</span>
          </div>
          <div class="relative h-1.5 w-full overflow-hidden rounded-full bg-gray-100" role="progressbar" aria-label="Upload progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
            <div class="upload-fill absolute inset-y-0 left-0 w-0 rounded-full bg-gradient-to-r from-blue-500 to-blue-600 transition-[width] duration-300 ease-out"></div>
          </div>
        </div>
        <button type="button" class="upload-remove shrink-0 rounded-md p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-400" aria-label="Remove file">
          <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
        </button>`;

      item.querySelector('.upload-name').textContent = name;
      list.appendChild(item);

      const fill = item.querySelector('.upload-fill');
      const pct = item.querySelector('.upload-pct');
      const bar = item.querySelector('[role="progressbar"]');
      const iconWrap = item.querySelector('.flex.h-9');
      const removeBtn = item.querySelector('.upload-remove');

      let value = 0;
      function remove() {
        item.classList.add('opacity-0', 'translate-x-2', 'transition', 'duration-300');
        setTimeout(() => item.remove(), 300);
      }
      removeBtn.addEventListener('click', remove);

      const timer = setInterval(() => {
        value = Math.min(100, value + Math.random() * 22);
        fill.style.width = value + '%';
        pct.textContent = Math.round(value) + '%';
        bar.setAttribute('aria-valuenow', String(Math.round(value)));
        if (value >= 100) {
          clearInterval(timer);
          setTimeout(() => {
            // flip to success
            fill.classList.remove('from-blue-500', 'to-blue-600');
            fill.classList.add('from-green-500', 'to-green-600');
            iconWrap.className = 'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-green-50 text-green-600';
            iconWrap.innerHTML = '<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
            pct.textContent = 'Done';
            pct.classList.remove('text-gray-500');
            pct.classList.add('text-green-600');
          }, 200);
        }
      }, 500);
    }

    window.addFile = addFile;

    // Demo: wire the file input + a couple of seed files
    const input = document.querySelector('input[type="file"]');
    if (input) {
      input.addEventListener('change', (e) => {
        Array.from(e.target.files).forEach((f) => addFile(f.name));
        e.target.value = '';
      });
    }
    addFile('design-system.fig');
    addFile('hero-photo.png');
  })();
</script>
97 lines UTF-8 · LF · Spaces: 2

AI-ready

Available to your agent.

This component is reachable through the DevSnips CLI, MCP server and Skills integrations — one registry, one resource path.

Resource path

devsnips://components/progress/upload-progress

Continue browsing

View all