Files
Htmx/docs/Components/Progress.md
T
2026-05-04 19:57:48 +05:00

4.8 KiB
Raw Blame History

Progress

A horizontal progress bar. Value is clamped to 0100. 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 0100
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-all do the work: re-render the component via HTMX on a polling interval or push updates via SSE.
  • For an indeterminate spinner, use Skeleton instead (it has animate-pulse built in).
  • For an indeterminate spinner, use Skeleton instead (it has animate-pulse built 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");
  }
}