Checkbox
An accessible tri-state checkbox primitive with full keyboard support and zero visual opinion.
Basic Checkbox (Uncontrolled)
Renders a native <button> with role="checkbox" and
aria-checked. Click or press Space to toggle.
DefaultChecked seeds the initial state.
<BitBhCheckbox aria-label="Remember me" class="checkbox">
<Template Context="c">
@if (c.IsChecked) { <span class="checkbox-tick">✓</span> }
</Template>
</BitBhCheckbox>
<BitBhCheckbox aria-label="Keep me signed in" DefaultChecked="true" class="checkbox">
<Template Context="c">
@if (c.IsChecked) { <span class="checkbox-tick">✓</span> }
</Template>
</BitBhCheckbox>/* All styles are consumer-authored — the library ships zero CSS */
.checkbox {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Box */
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.15rem;
height: 1.15rem;
border: 1px solid #d4d4d8;
border-radius: 0.25rem;
background: #fff;
color: #fff;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
flex-shrink: 0;
}
.checkbox:hover { border-color: #a1a1af; }
.checkbox:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
/* Hooks emitted by BitBhCheckbox */
.checkbox[data-state="checked"],
.checkbox[data-state="indeterminate"] {
background: #2563eb;
border-color: #2563eb;
}
.checkbox-tick,
.checkbox-dash {
font-size: 0.85rem;
line-height: 1;
}Controlled
Drive state externally with Checked + OnCheckedChange.
<BitBhCheckbox aria-label="Accept the terms" Checked="@acceptedTerms"
OnCheckedChange="v => acceptedTerms = v"
class="checkbox">
<Template Context="c">
@if (c.IsChecked) { <span class="checkbox-tick">✓</span> }
</Template>
</BitBhCheckbox>
@code {
private bool acceptedTerms;
}/* All styles are consumer-authored — the library ships zero CSS */
.checkbox {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Box */
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.15rem;
height: 1.15rem;
border: 1px solid #d4d4d8;
border-radius: 0.25rem;
background: #fff;
color: #fff;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
flex-shrink: 0;
}
.checkbox:hover { border-color: #a1a1af; }
.checkbox:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
/* Hooks emitted by BitBhCheckbox */
.checkbox[data-state="checked"],
.checkbox[data-state="indeterminate"] {
background: #2563eb;
border-color: #2563eb;
}
.checkbox-tick,
.checkbox-dash {
font-size: 0.85rem;
line-height: 1;
}
/* Status text + external toggle button used to drive the controlled checkbox */
.demo-output {
font-size: 0.875rem;
color: #71717a;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
background: #fff;
color: #18181b;
cursor: pointer;
}
.btn:hover { background: #f4f4f5; border-color: #d4d4d8; }
.btn:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.btn:active { background: #e4e4e7; }Disabled
Set Disabled="true" to block interaction. Receives the native
disabled attribute and a data-disabled hook.
<BitBhCheckbox aria-label="Unavailable option" Disabled="true" class="checkbox">
<Template Context="c">
@if (c.IsChecked) { <span class="checkbox-tick">✓</span> }
</Template>
</BitBhCheckbox>
<BitBhCheckbox aria-label="Locked option" DefaultChecked="true" Disabled="true" class="checkbox">
<Template Context="c">
@if (c.IsChecked) { <span class="checkbox-tick">✓</span> }
</Template>
</BitBhCheckbox>/* All styles are consumer-authored — the library ships zero CSS */
.checkbox {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Box */
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.15rem;
height: 1.15rem;
border: 1px solid #d4d4d8;
border-radius: 0.25rem;
background: #fff;
color: #fff;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
flex-shrink: 0;
}
.checkbox:hover { border-color: #a1a1af; }
.checkbox:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
/* Hooks emitted by BitBhCheckbox */
.checkbox[data-state="checked"],
.checkbox[data-state="indeterminate"] {
background: #2563eb;
border-color: #2563eb;
}
.checkbox-tick,
.checkbox-dash {
font-size: 0.85rem;
line-height: 1;
}
/* Hooks emitted when Disabled="true" */
.checkbox[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}Indeterminate (Select All pattern)
Setting Indeterminate="true" emits aria-checked="mixed"
and data-state="indeterminate". The classic use-case is a "select all"
checkbox that reflects partial child selection. Clicking an indeterminate checkbox
advances it to checked.
<div style="display:flex; flex-direction:column; gap:0.5rem;">
<label class="checkbox-label">
<BitBhCheckbox Checked="@SelectAllChecked"
Indeterminate="@SelectAllIndeterminate"
OnCheckedChange="HandleSelectAll"
class="checkbox">
<Template Context="c">
@if (c.IsIndeterminate) { <span class="checkbox-dash">–</span> }
else if (c.IsChecked) { <span class="checkbox-tick">✓</span> }
</Template>
</BitBhCheckbox>
<strong>Select all</strong>
</label>
<div style="display:flex; flex-direction:column; gap:0.4rem; margin-left:1.6rem;">
@for (var i = 0; i < items.Length; i++)
{
var captured = i;
<label class="checkbox-label">
<BitBhCheckbox Checked="@items[captured]"
OnCheckedChange="v => HandleItemChange(captured, v)"
class="checkbox">
<Template Context="c">
@if (c.IsChecked) { <span class="checkbox-tick">✓</span> }
</Template>
</BitBhCheckbox>
<span>@($"Item {captured + 1}")</span>
</label>
}
</div>
</div>
@code {
private bool[] items = new[] { false, false, false };
private bool SelectAllChecked => items.All(x => x);
private bool SelectAllIndeterminate => items.Any(x => x) && !SelectAllChecked;
private void HandleSelectAll(bool checkAll)
{
for (var i = 0; i < items.Length; i++) items[i] = checkAll;
}
private void HandleItemChange(int index, bool value) => items[index] = value;
}/* All styles are consumer-authored — the library ships zero CSS */
.checkbox {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Box */
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.15rem;
height: 1.15rem;
border: 1px solid #d4d4d8;
border-radius: 0.25rem;
background: #fff;
color: #fff;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
flex-shrink: 0;
}
.checkbox:hover { border-color: #a1a1af; }
.checkbox:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
/* Hooks emitted by BitBhCheckbox */
.checkbox[data-state="checked"],
.checkbox[data-state="indeterminate"] {
background: #2563eb;
border-color: #2563eb;
}
.checkbox-tick,
.checkbox-dash {
font-size: 0.85rem;
line-height: 1;
}
/* Wrapper that aligns each checkbox with its text label */
.checkbox-label {
display: inline-flex;
align-items: center;
gap: 0.55rem;
cursor: pointer;
user-select: none;
font-size: 0.9rem;
}
/* The dash glyph rendered inside the box for the mixed state */
.checkbox-dash {
font-size: 1rem;
font-weight: 700;
line-height: 0;
}With label
Wrap the checkbox in a <label> so clicking the text toggles the
checkbox — standard HTML behaviour.
<label class="checkbox-label">
<BitBhCheckbox class="checkbox">
<Template Context="c">
@if (c.IsChecked) { <span class="checkbox-tick">✓</span> }
</Template>
</BitBhCheckbox>
<span>Send me product updates</span>
</label>/* All styles are consumer-authored — the library ships zero CSS */
.checkbox {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Box */
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.15rem;
height: 1.15rem;
border: 1px solid #d4d4d8;
border-radius: 0.25rem;
background: #fff;
color: #fff;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
flex-shrink: 0;
}
.checkbox:hover { border-color: #a1a1af; }
.checkbox:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
/* Hooks emitted by BitBhCheckbox */
.checkbox[data-state="checked"],
.checkbox[data-state="indeterminate"] {
background: #2563eb;
border-color: #2563eb;
}
.checkbox-tick,
.checkbox-dash {
font-size: 0.85rem;
line-height: 1;
}
/* Wrapper that aligns the checkbox with its text label */
.checkbox-label {
display: inline-flex;
align-items: center;
gap: 0.55rem;
cursor: pointer;
user-select: none;
font-size: 0.9rem;
}Form Submission
Set Name to render an adjacent <input type="hidden">
so the checkbox state participates in plain form posts. The hidden input is only
present when the checkbox is checked or indeterminate, mirroring native checkbox behaviour.
<form method="post">
<label class="checkbox-label">
<BitBhCheckbox Name="agree" Value="yes"
class="checkbox">
<Template Context="c">
@if (c.IsChecked) { <span class="checkbox-tick">✓</span> }
</Template>
</BitBhCheckbox>
<span>I agree to the terms</span>
</label>
<button type="submit">Submit</button>
</form>
@* When checked, posts: agree=yes *@/* All styles are consumer-authored — the library ships zero CSS */
.checkbox {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Box */
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.15rem;
height: 1.15rem;
border: 1px solid #d4d4d8;
border-radius: 0.25rem;
background: #fff;
color: #fff;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
flex-shrink: 0;
}
.checkbox:hover { border-color: #a1a1af; }
.checkbox:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
/* Hooks emitted by BitBhCheckbox */
.checkbox[data-state="checked"],
.checkbox[data-state="indeterminate"] {
background: #2563eb;
border-color: #2563eb;
}
.checkbox-tick,
.checkbox-dash {
font-size: 0.85rem;
line-height: 1;
}
/* Wrapper that aligns the checkbox with its text label */
.checkbox-label {
display: inline-flex;
align-items: center;
gap: 0.55rem;
cursor: pointer;
user-select: none;
font-size: 0.9rem;
}
/* Submit button for the form post */
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
background: #fff;
color: #18181b;
cursor: pointer;
}
.btn:hover { background: #f4f4f5; border-color: #d4d4d8; }
.btn:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.btn:active { background: #e4e4e7; }
.btn-primary {
background: #2563eb;
border-color: #2563eb;
color: white;
}
.btn-primary:hover {
background: #1d4ed8;
border-color: #1d4ed8;
}
.demo-output {
font-size: 0.875rem;
color: #71717a;
}Group with select all
BitBhCheckboxGroup binds one collection with @bind-Values and
renders role="group"; each BitBhCheckbox with a Value
toggles that value. SelectAll="true" turns a checkbox into the tri-state
parent — checked when every value is selected, aria-checked="mixed" /
data-state="indeterminate" when only some are. BitBhCheckboxIndicator
renders only while its checkbox is checked or mixed, and mirrors its data-state.
<BitBhCheckboxGroup @bind-Values="toppings" aria-label="Pizza toppings" class="checkbox-group">
@* Tri-state parent: checked / mixed / unchecked from the group's values *@
<BitBhCheckbox SelectAll="true" class="checkbox-row checkbox-row-all">
<span class="checkbox-box"><BitBhCheckboxIndicator class="checkbox-indicator" /></span>
All toppings
</BitBhCheckbox>
<div class="checkbox-group-items">
@foreach (var topping in allToppings)
{
<BitBhCheckbox Value="@topping" class="checkbox-row">
<span class="checkbox-box"><BitBhCheckboxIndicator class="checkbox-indicator" /></span>
@topping
</BitBhCheckbox>
}
</div>
</BitBhCheckboxGroup>
<span class="demo-output">
Values: <strong>@(toppings.Count == 0 ? "(none)" : string.Join(", ", toppings))</strong>
</span>
@code {
private string[] allToppings = new[] { "Cheese", "Mushrooms", "Olives", "Peppers" };
private IReadOnlyCollection<string> toppings = new[] { "Cheese", "Olives" };
}/* All styles are consumer-authored — the library ships zero CSS */
.checkbox-group { display: flex; flex-direction: column; gap: 0.5rem; }
.checkbox-group-items { display: flex; flex-direction: column; gap: 0.4rem; padding-left: 1.6rem; }
/* The checkbox button holds the box and the label text (its accessible name) */
.checkbox-row {
all: unset;
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: 0.55rem;
border-radius: 0.25rem;
font-size: 0.9rem;
cursor: pointer;
user-select: none;
}
.checkbox-row:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.checkbox-row-all { font-weight: 600; }
.checkbox-box {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.15rem;
height: 1.15rem;
flex-shrink: 0;
border: 1px solid #d4d4d8;
border-radius: 0.25rem;
background: #fff;
color: #fff;
transition: background 120ms ease, border-color 120ms ease;
}
.checkbox-row:hover .checkbox-box { border-color: #a1a1af; }
/* Hooks emitted by BitBhCheckbox */
.checkbox-row[data-state="checked"] .checkbox-box,
.checkbox-row[data-state="indeterminate"] .checkbox-box {
background: #2563eb;
border-color: #2563eb;
}
/* BitBhCheckboxIndicator mirrors the state, so CSS picks the glyph */
.checkbox-indicator { font-size: 0.8rem; font-weight: 700; line-height: 1; }
.checkbox-indicator[data-state="checked"]::before { content: "\2713"; } /* ✓ */
.checkbox-indicator[data-state="indeterminate"]::before { content: "\2013"; } /* – */
.demo-output { font-size: 0.875rem; color: #71717a; }