Skip to content

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.

Upload Queue
<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>