ee8797c142
Co-authored-by: Copilot <copilot@github.com>
4.8 KiB
4.8 KiB
Progress
A horizontal progress bar. Value is clamped to 0–100. Three sizes control the bar height.
HTML structure
div.w-full.bg-secondary.rounded-full.overflow-hidden.{size class}
div.bg-primary.rounded-full.h-full.transition-all[style="width: {value}%"]
CSS mechanics
| Class | Effect |
|---|---|
bg-secondary |
Neutral track color |
bg-primary |
Filled indicator color |
rounded-full overflow-hidden |
Pill-shaped track; fills also become pill-shaped |
transition-all |
Smooth animation when width changes |
Size classes applied to the outer track:
| Size | Class | Height |
|---|---|---|
sm |
h-1.5 |
6 px |
default |
h-2.5 |
10 px |
lg |
h-4 |
16 px |
Constructor signature
public Progress(int value, string size = "default")
| Parameter | Description |
|---|---|
value |
Fill percentage; clamped to 0–100 |
size |
"sm" / "default" / "lg" |
Usage examples
Inline usage
new Progress(value: 72)
new Progress(value: 40, size: "sm")
new Progress(value: 100, size: "lg")
Inside a Card
new Card(
title: "Disk usage",
content: $"""
<div class="mb-2 flex justify-between text-sm">
<span>Used</span>
<span>{used} GB / {total} GB</span>
</div>
{progressHtml}
""")
(Pre-render the Progress to a string using HtmxRenderContext and ArrayBufferWriter<byte>.)
HTMX live update
<div id="progress-bar"
hx-get="/job/42/progress"
hx-trigger="every 1s"
hx-swap="outerHTML">
$$ProgressBar$$
</div>
The endpoint returns a partial re-render of this fragment with the updated value.
Tips and tricks
- Values below 0 are treated as 0; values above 100 are treated as 100 — no manual clamping needed.
- Use
size: "sm"for compact UI areas such as table rows. - To animate progress smoothly, let
transition-alldo the work: re-render the component via HTMX on a polling interval or push updates via SSE. - For an indeterminate spinner, use
Skeletoninstead (it hasanimate-pulsebuilt in). - For an indeterminate spinner, use
Skeletoninstead (it hasanimate-pulsebuilt in).
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");
}
}