Progress / Meter
Report task completion with a progress bar, or a measurement within a known range with a meter.
Determinate progress
BitBhProgress renders role="progressbar" with
aria-valuenow/min/max and a percentage
aria-valuetext. It exposes --bit-bh-progress (a unitless 0–100 number) and
data-state="loading|complete"; size the BitBhProgressIndicator with
calc(var(--bit-bh-progress) * 1%). Name it with aria-labelledby.
<div class="progress-card">
<div class="progress-header">
<span class="progress-label" id="profile-label">Profile completion</span>
<span class="progress-status">@profile%</span>
</div>
<BitBhProgress Value="profile" aria-labelledby="profile-label" class="progress">
<BitBhProgressIndicator class="progress-indicator" />
</BitBhProgress>
<div class="progress-actions">
<button type="button" class="progress-btn" @onclick="() => profile = Math.Max(0, profile - 20)">− 20%</button>
<button type="button" class="progress-btn" @onclick="() => profile = Math.Min(100, profile + 20)">+ 20%</button>
</div>
</div>
@code {
private double profile = 60;
}/* All styles are consumer-authored — the library ships zero CSS */
.progress {
position: relative;
overflow: hidden;
width: 100%;
height: 0.6rem;
border-radius: 999px;
background: #e4e4e7;
}
/* --bit-bh-progress is a unitless 0–100 number set inline by BitBhProgress and
BitBhProgressIndicator; multiply by 1% to size the fill. */
.progress-indicator {
width: calc(var(--bit-bh-progress, 0) * 1%);
height: 100%;
border-radius: inherit;
background: #2563eb;
transition: width 250ms ease, background-color 250ms ease;
}
/* data-state="loading | complete | indeterminate" */
.progress-indicator[data-state="complete"] { background: #16a34a; }
/* Labelled card around the bar */
.progress-card {
display: grid;
gap: 0.6rem;
width: 100%;
max-width: 24rem;
padding: 1rem;
border: 1px solid #e4e4e7;
border-radius: 0.6rem;
background: #fff;
}
.progress-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
font-size: 0.875rem;
}
.progress-label { font-weight: 600; color: #18181b; }
.progress-status {
color: #71717a;
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
}
.progress-actions { display: flex; gap: 0.5rem; }
.progress-btn {
all: unset;
box-sizing: border-box;
display: inline-flex;
align-items: center;
padding: 0.35rem 0.75rem;
border: 1px solid #e4e4e7;
border-radius: 0.45rem;
background: #fff;
color: #3f3f46;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
}
.progress-btn:hover { background: #f4f4f5; }
.progress-btn:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.progress-btn:disabled { opacity: 0.5; cursor: not-allowed; }Simulated upload
A timer drives Value in megabytes against Max="8"; ValueText
turns it into "2.4 of 8 MB uploaded" for screen readers. The upload starts with
Value="null" (indeterminate) while "preparing". Template receives the
computed percentage (double?) to draw the label inside the bar. The page
implements IDisposable to stop the timer.
@implements IDisposable
<div class="progress-card">
<div class="progress-header">
<span class="progress-label" id="upload-label">quarterly-report.pdf</span>
<span class="progress-status">@UploadStatus()</span>
</div>
<BitBhProgress Value="uploaded" Max="FileSizeMb"
ValueText="@((v, max) => $"{v:0.0} of {max:0} MB uploaded")"
aria-labelledby="upload-label" class="progress progress-lg">
<Template Context="percent">
<BitBhProgressIndicator class="progress-indicator" />
<span class="progress-percent">@(percent is null ? "" : $"{percent:0}%")</span>
</Template>
</BitBhProgress>
<div class="progress-actions">
<button type="button" class="progress-btn progress-btn-primary"
disabled="@uploading" @onclick="StartUpload">
@(uploaded >= FileSizeMb ? "Upload again" : "Start upload")
</button>
<button type="button" class="progress-btn"
disabled="@(!uploading)" @onclick="CancelUpload">Cancel</button>
</div>
</div>
@code {
private const double FileSizeMb = 8;
private double? uploaded = 0; // null = indeterminate
private bool uploading;
private int uploadTicks;
private System.Threading.Timer? uploadTimer;
private string UploadStatus()
{
if (uploaded is null) return "Preparing…";
if (uploaded >= FileSizeMb) return "Complete";
if (!uploading) return "Ready";
return $"{uploaded:0.0} of {FileSizeMb:0} MB";
}
private void StartUpload()
{
uploadTimer?.Dispose();
uploading = true;
uploaded = null; // indeterminate while "preparing"
uploadTicks = 0;
uploadTimer = new System.Threading.Timer(_ => InvokeAsync(UploadTick), null, 150, 150);
}
private void UploadTick()
{
if (!uploading) return;
uploadTicks++;
if (uploadTicks > 6) // ~1 s of "preparing" first
{
uploaded = Math.Min(FileSizeMb, (uploaded ?? 0) + 0.2);
if (uploaded >= FileSizeMb) StopTimer();
}
StateHasChanged();
}
private void CancelUpload()
{
StopTimer();
uploaded = 0;
}
private void StopTimer()
{
uploading = false;
uploadTimer?.Dispose();
uploadTimer = null;
}
public void Dispose() => uploadTimer?.Dispose();
}/* All styles are consumer-authored — the library ships zero CSS */
.progress {
position: relative;
overflow: hidden;
width: 100%;
height: 0.6rem;
border-radius: 999px;
background: #e4e4e7;
}
/* --bit-bh-progress is a unitless 0–100 number set inline by BitBhProgress and
BitBhProgressIndicator; multiply by 1% to size the fill. */
.progress-indicator {
width: calc(var(--bit-bh-progress, 0) * 1%);
height: 100%;
border-radius: inherit;
background: #2563eb;
transition: width 250ms ease, background-color 250ms ease;
}
/* data-state="loading | complete | indeterminate" */
.progress-indicator[data-state="complete"] { background: #16a34a; }
.progress-indicator[data-state="indeterminate"] {
width: 40%;
animation: progress-demo-slide 1.2s ease-in-out infinite;
}
@keyframes progress-demo-slide {
from { transform: translateX(-100%); }
to { transform: translateX(250%); }
}
@media (prefers-reduced-motion: reduce) {
.progress-indicator[data-state="indeterminate"] { animation-duration: 3s; }
}
/* Tall bar with the percentage drawn through BitBhProgress's Template */
.progress-lg { height: 1.25rem; }
.progress-lg .progress-indicator { background: #bfdbfe; }
.progress-lg .progress-indicator[data-state="complete"] { background: #bbf7d0; }
.progress-percent {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 0.6rem;
color: #1e3a8a;
font-size: 0.72rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
/* Labelled card around the bar */
.progress-card {
display: grid;
gap: 0.6rem;
width: 100%;
max-width: 24rem;
padding: 1rem;
border: 1px solid #e4e4e7;
border-radius: 0.6rem;
background: #fff;
}
.progress-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
font-size: 0.875rem;
}
.progress-label { font-weight: 600; color: #18181b; }
.progress-status {
color: #71717a;
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
}
.progress-actions { display: flex; gap: 0.5rem; }
.progress-btn {
all: unset;
box-sizing: border-box;
display: inline-flex;
align-items: center;
padding: 0.35rem 0.75rem;
border: 1px solid #e4e4e7;
border-radius: 0.45rem;
background: #fff;
color: #3f3f46;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
}
.progress-btn:hover { background: #f4f4f5; }
.progress-btn:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.progress-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.progress-btn-primary { background: #2563eb; border-color: #2563eb; color: #fff; }
.progress-btn-primary:hover { background: #1d4ed8; }Indeterminate
Leave Value unset (null) when the duration is unknown:
aria-valuenow is omitted, data-state="indeterminate" is set and
--bit-bh-progress is absent, so CSS can switch to an animation. Honor
prefers-reduced-motion.
<div class="progress-card">
<div class="progress-header">
<span class="progress-label" id="sync-label">Syncing mailbox</span>
<span class="progress-status">Waiting for the server…</span>
</div>
@* No Value: indeterminate *@
<BitBhProgress aria-labelledby="sync-label" class="progress">
<BitBhProgressIndicator class="progress-indicator" />
</BitBhProgress>
</div>/* All styles are consumer-authored — the library ships zero CSS */
.progress {
position: relative;
overflow: hidden;
width: 100%;
height: 0.6rem;
border-radius: 999px;
background: #e4e4e7;
}
/* --bit-bh-progress is a unitless 0–100 number set inline by BitBhProgress and
BitBhProgressIndicator; multiply by 1% to size the fill. */
.progress-indicator {
width: calc(var(--bit-bh-progress, 0) * 1%);
height: 100%;
border-radius: inherit;
background: #2563eb;
transition: width 250ms ease, background-color 250ms ease;
}
/* data-state="loading | complete | indeterminate" */
.progress-indicator[data-state="complete"] { background: #16a34a; }
.progress-indicator[data-state="indeterminate"] {
width: 40%;
animation: progress-demo-slide 1.2s ease-in-out infinite;
}
@keyframes progress-demo-slide {
from { transform: translateX(-100%); }
to { transform: translateX(250%); }
}
@media (prefers-reduced-motion: reduce) {
.progress-indicator[data-state="indeterminate"] { animation-duration: 3s; }
}
/* Labelled card around the bar */
.progress-card {
display: grid;
gap: 0.6rem;
width: 100%;
max-width: 24rem;
padding: 1rem;
border: 1px solid #e4e4e7;
border-radius: 0.6rem;
background: #fff;
}
.progress-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
font-size: 0.875rem;
}
.progress-label { font-weight: 600; color: #18181b; }
.progress-status {
color: #71717a;
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
}Meter: disk usage
BitBhMeter (role="meter") shows a scalar within a known range — not task
progress. Low and High split the range into
data-level="low|optimum|high", and --bit-bh-meter (0–100) sizes the fill.
ValueText gives a readable value.
- System 231 of 256 GB
- Projects 118 of 256 GB
- Backup 37 of 256 GB
<ul class="meter-list">
@foreach (var drive in drives)
{
<li class="meter-row">
<div class="progress-header">
<span class="progress-label" id="@($"drive-{drive.Id}")">@drive.Name</span>
<span class="progress-status">@drive.Used of 256 GB</span>
</div>
<BitBhMeter Value="drive.Used" Max="256" Low="64" High="200"
ValueText="@(v => $"{v} of 256 GB used")"
aria-labelledby="@($"drive-{drive.Id}")" class="meter">
<span class="meter-fill"></span>
</BitBhMeter>
</li>
}
</ul>
@code {
private readonly (string Id, string Name, double Used)[] drives =
[("system", "System", 231), ("projects", "Projects", 118), ("backup", "Backup", 37)];
}/* All styles are consumer-authored — the library ships zero CSS */
.meter {
position: relative;
overflow: hidden;
width: 100%;
height: 0.5rem;
border-radius: 999px;
background: #e4e4e7;
}
/* --bit-bh-meter (0–100) is set inline on the meter and inherited by the fill. */
.meter-fill {
display: block;
width: calc(var(--bit-bh-meter) * 1%);
height: 100%;
border-radius: inherit;
background: #2563eb;
transition: width 250ms ease, background-color 250ms ease;
}
/* data-level="low | optimum | high" (below Low / between / above High) */
.meter[data-level="low"] .meter-fill { background: #16a34a; }
.meter[data-level="high"] .meter-fill { background: #dc2626; }
.meter-list {
display: grid;
gap: 1rem;
width: 100%;
max-width: 24rem;
margin: 0;
padding: 0;
list-style: none;
}
.meter-row { display: grid; gap: 0.4rem; }
.progress-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
font-size: 0.875rem;
}
.progress-label { font-weight: 600; color: #18181b; }
.progress-status { color: #71717a; font-size: 0.8rem; font-variant-numeric: tabular-nums; }Meter: password strength
A 0–4 score with Low="2" and High="3": weak scores are
low, fair ones optimum, a full score high.
BitBhMeter's Template receives the percentage, used here to fill
four segments.
<div class="strength">
<label for="pw" class="progress-label">Choose a password</label>
<input id="pw" type="password" class="strength-input" autocomplete="new-password"
@bind="password" @bind:event="oninput" aria-describedby="pw-strength" />
<BitBhMeter Value="Score()" Max="4" Low="2" High="3"
ValueText="@(v => StrengthLabels[(int)v])"
aria-label="Password strength" class="strength-meter">
<Template Context="percent">
@for (var i = 1; i <= 4; i++)
{
<span class="strength-segment" data-filled="@(percent >= i * 25)"></span>
}
</Template>
</BitBhMeter>
<span class="progress-status" id="pw-strength">Strength: <strong>@(StrengthLabels[Score()])</strong></span>
</div>
@code {
private string password = "";
private static readonly string[] StrengthLabels = ["Very weak", "Weak", "Fair", "Good", "Strong"];
private int Score()
{
if (password.Length == 0) return 0;
var score = 0;
if (password.Length >= 8) score++;
if (password.Any(char.IsUpper) && password.Any(char.IsLower)) score++;
if (password.Any(char.IsDigit)) score++;
if (password.Any(c => !char.IsLetterOrDigit(c))) score++;
return score;
}
}/* All styles are consumer-authored — the library ships zero CSS */
.strength {
display: grid;
gap: 0.5rem;
width: 100%;
max-width: 20rem;
}
.strength-input {
padding: 0.5rem 0.75rem;
border: 1px solid #d4d4d8;
border-radius: 0.45rem;
background: #fff;
font: inherit;
font-size: 0.9rem;
}
.strength-input:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
/* BitBhMeter root laid out as four segments */
.strength-meter {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.3rem;
}
.strength-segment {
height: 0.375rem;
border-radius: 999px;
background: #e4e4e7;
transition: background-color 200ms ease;
}
/* data-level from Low="2" / High="3"; data-filled from the Template */
.strength-meter[data-level="low"] .strength-segment[data-filled] { background: #dc2626; }
.strength-meter[data-level="optimum"] .strength-segment[data-filled] { background: #d97706; }
.strength-meter[data-level="high"] .strength-segment[data-filled] { background: #16a34a; }
.progress-label { font-weight: 600; color: #18181b; }
.progress-status { color: #71717a; font-size: 0.8rem; font-variant-numeric: tabular-nums; }