Co-authored-by: Copilot <copilot@github.com>
4.6 KiB
Progress
A horizontal bar that fills from left to right to show how complete something is. Use it for upload progress, onboarding checklists, storage usage, or anything that has a percentage value.
Quick example
new Progress(value: 72)
All the options
public Progress(int value, string size = "default")
| Parameter | What it does |
|---|---|
value |
How filled the bar is, from 0 to 100. Values outside this range are clamped automatically. |
size |
Height of the bar: "sm" (6px), "default" (10px), or "lg" (16px). |
Real-world examples
Disk usage inside a Card
// Pre-render the Progress bar to HTML
var w = new System.Buffers.ArrayBufferWriter<byte>();
new Progress(value: usedPercent, size: "lg").Render(new HtmxRenderContext(w));
var progressHtml = System.Text.Encoding.UTF8.GetString(w.WrittenSpan);
new Card(
title: "Storage",
content: $"""
<div class="mb-2 flex justify-between text-sm">
<span>Used</span>
<span>{usedGb} GB / {totalGb} GB</span>
</div>
{progressHtml}
""")
Live progress bar (HTMX polling)
Wrap the component in a polling <div> that swaps the fragment every second:
<div id="job-progress"
hx-get="/jobs/42/progress"
hx-trigger="every 1s"
hx-swap="outerHTML">
$$ProgressBar$$
</div>
The handler returns a fresh render of the component with the updated value. The transition-all CSS on the fill makes the change smooth.
Three sizes side by side
new Progress(value: 40, size: "sm") // compact, good for table rows
new Progress(value: 60) // standard
new Progress(value: 80, size: "lg") // prominent
How it works
Progress is two nested <div> elements. The outer one is the grey track; the inner one is the filled bar. The fill width is set as an inline style="width: {value}%" so no JavaScript is required. The transition-all class makes the bar animate smoothly when the value changes via an HTMX swap.
Complete page example
Templates/JobStatusPage.htmx
<div class="max-w-md mx-auto py-10">
<h1 class="text-2xl font-bold mb-2">Processing</h1>
<p class="text-sm text-muted-foreground mb-6">$$StatusText$$</p>
<div class="mb-2 flex justify-between text-sm">
<span>Progress</span>
<span>$$ProgressLabel$$</span>
</div>
$$ProgressBar$$
$$DoneAlert$$
</div>
Templates/JobStatusPage.htmx.cs
namespace Htmx.ApiDemo.Templates;
public sealed class JobStatusPage : JobStatusPageBase
{
private readonly byte[] _statusText;
private readonly byte[] _progressLabel;
private readonly IHtmxComponent _progressBar;
private readonly IHtmxComponent _doneAlert;
public JobStatusPage(int percent, string statusText)
{
_statusText = System.Net.WebUtility.HtmlEncode(statusText).ToUtf8Bytes();
_progressLabel = $"{percent}%".ToUtf8Bytes();
_progressBar = new Components.Progress(value: percent);
_doneAlert = percent >= 100
? new Components.Alert(title: "Complete!", description: "Your export is ready.")
: HtmxEmpty.Instance;
}
protected override void RenderStatusText(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_statusText);
protected override void RenderProgressLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_progressLabel);
protected override void RenderProgressBar(HtmxRenderContext ctx) => _progressBar.Render(ctx.Next());
protected override void RenderDoneAlert(HtmxRenderContext ctx) => _doneAlert.Render(ctx.Next());
}
GET handler with HTMX polling
[Handler]
[MapGet("/jobs/{jobId}/status")]
public static partial class GetJobStatusHandler
{
public record Query([property: FromRoute] string JobId);
private static async Task<IResult> HandleAsync(
Query q, HttpContext ctx, JobQueue jobs, CancellationToken ct)
{
var job = await jobs.GetAsync(q.JobId, ct);
if (job is null) return Results.NotFound();
var page = new JobStatusPage(job.PercentComplete, job.StatusText);
// If polling (HTMX partial), only return the progress fragment
if (ctx.Request.Headers.ContainsKey("HX-Request"))
{
// Stop polling when done
if (job.PercentComplete >= 100)
ctx.Response.Headers.Append("HX-Trigger", "jobComplete");
return await ctx.WriteHtmxPage(page, title: "Processing");
}
// Full page load — include polling trigger
ctx.Response.Headers.Append("HX-Trigger-After-Settle",
"""{"startPolling": {"interval": 1000, "target": "#progress-region"}}""");
return await ctx.WriteHtmxPage(page, title: "Processing");
}
}