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

What it does

Capabilities

  • Per-file progress bars
  • Percentage labels
  • Success state transition
  • Remove mid-upload
  • Programmatic addFile()
  • Role=progressbar
  • Aria-valuenow
  • Tabular-nums

Preview

Live component

Interactive preview
Open preview
9:41 100%
devsnips.site/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
DevSnips AI ready Claude · Cursor · Windsurf

Agent skill & Model Context Protocol

Pass this component straight into your AI workflow. DevSnips exposes each resource as a structured, addressable registry entry, so your agent installs the real files instead of approximating them.

devsnips://components/progress/upload-progress
Prompt instruction
I want to use this DevSnips resource in production:

Page:
https://devsnips.site/library/tailwind/components/progress/upload-progress/

Install: npx devsnips add
Tailwind/Components/Progress/upload-progress

This is the 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.

Use this DevSnips resource as the starting point for my project.
Adapt it to my project's stack and conventions, fix any issues,
and make it production-ready while preserving its intended design
and behavior.

Paste into ChatGPT, Claude or your agent — it already carries the page URL and the exact install path.

Resource URI
devsnips://components/progress/upload-progress