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
9:41 100%
devsnips.dev/library/tailwind/components/progress/upload-progress/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add Tailwind/Components/Progress/upload-progress Tailwind/Components/Progress/upload-progress <!--
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> # Upload Progress
Upload progress indicator.
## Usage
```bash
npx devsnips add Tailwind/Components/Progress/upload-progress
```
Copy from `code.html`.
## File Structure
`code.html` · `preview.html` · `metadata.json` · `README.md` 97 lines UTF-8 · LF · Spaces: 2
Continue browsing