Progress Bar Upload Queue
Files upload one after another, and a data-state attribute on each row drives its look from CSS. Setting value to null makes a bar indeterminate while the server prepares the file, then the percentage takes over. Each bar is named by its file name through aria-labelledby, and the status text carries data-value-text.
-
report.pdf Queued -
holiday.mp4 Queued -
backup.zip Queued
Code
The following example assumes that you have imported the component and set up the theme.
<div class="upload-queue">24 collapsed lines
<ul class="upload-list"> <li class="upload-item" data-state="queued"> <sv-progress-bar aria-labelledby="upload-name-1" value="0"> <span id="upload-name-1">report.pdf</span> <span class="upload-status" data-value-text>Queued</span> </sv-progress-bar> </li>
<li class="upload-item" data-state="queued"> <sv-progress-bar aria-labelledby="upload-name-2" value="0"> <span id="upload-name-2">holiday.mp4</span> <span class="upload-status" data-value-text>Queued</span> </sv-progress-bar> </li>
<li class="upload-item" data-state="queued"> <sv-progress-bar aria-labelledby="upload-name-3" value="0"> <span id="upload-name-3">backup.zip</span> <span class="upload-status" data-value-text>Queued</span> </sv-progress-bar> </li> </ul>
<button type="button" class="upload-button">Upload</button></div>
<script type="application/x-typescript" module>39 collapsed lines
const queue = document.querySelector<HTMLElement>(".upload-queue")!; const uploadItems = queue.querySelectorAll<HTMLElement>(".upload-item"); const uploadButton = queue.querySelector<HTMLButtonElement>(".upload-button")!;
uploadButton.addEventListener("click", async () => { uploadButton.disabled = true;
for (const uploadItem of uploadItems) { await upload(uploadItem); }
uploadButton.disabled = false; });
async function upload(uploadItem: HTMLElement) { const progressBar = uploadItem.querySelector<ProgressBar>("sv-progress-bar")!; const statusElement = uploadItem.querySelector(".upload-status")!;
uploadItem.dataset.state = "preparing"; statusElement.textContent = "Preparing"; progressBar.value = null; await wait(1200);
uploadItem.dataset.state = "uploading";
for (let percentage = 0; percentage < 100; percentage += 4) { statusElement.textContent = `${percentage}%`; progressBar.value = percentage; await wait(60); }
uploadItem.dataset.state = "done"; statusElement.textContent = "Done"; progressBar.value = 100; }
function wait(milliseconds: number) { return new Promise(resolve => setTimeout(resolve, milliseconds)); }</script>
<style>66 collapsed lines
.upload-queue { --done-color: #22c55e;
display: grid; gap: 1.25rem; max-inline-size: 24rem; padding: 1.25rem; margin: auto; background-color: var(--sv-gray-7); border: 1px solid var(--sv-gray-5); border-radius: var(--sv-bdr-rad-lg); }
.upload-list { display: grid; gap: 1rem; padding: 0; margin: 0; list-style: none; }
.upload-item { font-size: 0.875rem;
.upload-status { font-family: monospace; color: var(--sv-gray-3); }
&[data-state="queued"] { opacity: 0.6; }
&[data-state="done"] { sv-progress-bar { --sv-progress-bar-fill-clr: var(--done-color); }
.upload-status { color: var(--done-color); } } }
.upload-button { justify-self: end; min-inline-size: 7rem; padding: 0.5em 1em; font-family: inherit; font-size: 0.875rem; color: var(--sv-accent-content); cursor: pointer; background-color: var(--sv-accent); border: none; border-radius: var(--sv-bdr-rad-md);
&:disabled { cursor: not-allowed; opacity: 0.5; }
&:focus-visible { outline: 2px solid var(--sv-accent); outline-offset: 2px; } }</style>