Switch
An accessible on/off toggle with full keyboard support and zero visual opinion.
Basic Switch (Uncontrolled)
Renders a native <button> with role="switch" and
aria-checked. Click or press Space/Enter to toggle.
DefaultChecked seeds the initial state.
<BitBhSwitch aria-label="Wi-Fi" DefaultChecked="false" class="switch">
<BitBhSwitchThumb class="switch-thumb" />
</BitBhSwitch>/* All styles are consumer-authored — the library ships zero CSS */
.switch {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Track */
position: relative;
display: inline-block;
width: 2.5rem;
height: 1.4rem;
border-radius: 999px;
background: #d4d4d8;
cursor: pointer;
transition: background 150ms ease;
/* Focus ring */
outline: none;
}
.switch:focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2563eb;
}
/* Hooks emitted by BitBhSwitch */
.switch[data-state="checked"] { background: #2563eb; }
.switch[data-disabled] { opacity: 0.5; cursor: not-allowed; }
/* Thumb — driven by data-state on the inner element */
.switch-thumb {
position: absolute;
top: 50%;
left: 0.15rem;
width: 1.1rem;
height: 1.1rem;
background: #fff;
border-radius: 999px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transform: translateY(-50%);
transition: left 150ms ease;
}
.switch-thumb[data-state="checked"] {
left: calc(100% - 1.1rem - 0.15rem);
}Controlled
Drive the state externally with Checked + OnCheckedChange.
Clicking either control updates the same state.
<BitBhSwitch aria-label="Notifications" Checked="@notificationsOn"
OnCheckedChange="v => notificationsOn = v"
class="switch">
<BitBhSwitchThumb class="switch-thumb" />
</BitBhSwitch>
@code {
private bool notificationsOn = true;
}/* All styles are consumer-authored — the library ships zero CSS */
.switch {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Track */
position: relative;
display: inline-block;
width: 2.5rem;
height: 1.4rem;
border-radius: 999px;
background: #d4d4d8;
cursor: pointer;
transition: background 150ms ease;
/* Focus ring */
outline: none;
}
.switch:focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2563eb;
}
/* Hooks emitted by BitBhSwitch */
.switch[data-state="checked"] { background: #2563eb; }
.switch[data-disabled] { opacity: 0.5; cursor: not-allowed; }
/* Thumb — driven by data-state on the inner element */
.switch-thumb {
position: absolute;
top: 50%;
left: 0.15rem;
width: 1.1rem;
height: 1.1rem;
background: #fff;
border-radius: 999px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transform: translateY(-50%);
transition: left 150ms ease;
}
.switch-thumb[data-state="checked"] {
left: calc(100% - 1.1rem - 0.15rem);
}
/* Controlled mode adds no special CSS hooks — data-state simply reflects
whatever Checked value you bind to. Below are the external controls. */
.demo-output { font-size: 0.875rem; color: #71717a; }
.btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.45rem 0.85rem;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
background: #fff;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
}
.btn:hover { background: #f4f4f5; }Disabled
Set Disabled="true" to block interaction. The button receives
the native disabled attribute and a data-disabled hook.
<BitBhSwitch aria-label="Location services" DefaultChecked="true" Disabled="true" class="switch">
<BitBhSwitchThumb class="switch-thumb" />
</BitBhSwitch>/* All styles are consumer-authored — the library ships zero CSS */
.switch {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Track */
position: relative;
display: inline-block;
width: 2.5rem;
height: 1.4rem;
border-radius: 999px;
background: #d4d4d8;
cursor: pointer;
transition: background 150ms ease;
/* Focus ring */
outline: none;
}
.switch:focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2563eb;
}
/* Hooks emitted by BitBhSwitch */
.switch[data-state="checked"] { background: #2563eb; }
/* Hooks emitted when Disabled="true". The disabled state still respects the
checked appearance, so the thumb stays in the "on" position on a
disabled-checked switch. */
.switch[data-disabled] { opacity: 0.5; cursor: not-allowed; }
/* Thumb — driven by data-state on the inner element */
.switch-thumb {
position: absolute;
top: 50%;
left: 0.15rem;
width: 1.1rem;
height: 1.1rem;
background: #fff;
border-radius: 999px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transform: translateY(-50%);
transition: left 150ms ease;
}
.switch-thumb[data-state="checked"] {
left: calc(100% - 1.1rem - 0.15rem);
}With label
Wrap the switch in a <label> to make the label clickable.
Clicking the label text toggles the switch — standard HTML behaviour.
<label class="switch-label">
<BitBhSwitch DefaultChecked="true" class="switch">
<BitBhSwitchThumb class="switch-thumb" />
</BitBhSwitch>
<span>Email me about updates</span>
</label>/* All styles are consumer-authored — the library ships zero CSS */
.switch {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Track */
position: relative;
display: inline-block;
width: 2.5rem;
height: 1.4rem;
border-radius: 999px;
background: #d4d4d8;
cursor: pointer;
transition: background 150ms ease;
/* Focus ring */
outline: none;
}
.switch:focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2563eb;
}
/* Hooks emitted by BitBhSwitch */
.switch[data-state="checked"] { background: #2563eb; }
.switch[data-disabled] { opacity: 0.5; cursor: not-allowed; }
/* Thumb — driven by data-state on the inner element */
.switch-thumb {
position: absolute;
top: 50%;
left: 0.15rem;
width: 1.1rem;
height: 1.1rem;
background: #fff;
border-radius: 999px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transform: translateY(-50%);
transition: left 150ms ease;
}
.switch-thumb[data-state="checked"] {
left: calc(100% - 1.1rem - 0.15rem);
}
/* Wrapper that aligns the switch with its text label */
.switch-label {
display: inline-flex;
align-items: center;
gap: 0.6rem;
cursor: pointer;
user-select: none;
font-size: 0.9rem;
}As a non-native element
Set As="div" to render as a div. The component automatically adds
role="switch", tabindex="0", and Space/Enter keyboard
handling so the switch remains fully accessible.
<BitBhSwitch aria-label="Dark mode" As="div" DefaultChecked="false" class="switch">
<BitBhSwitchThumb class="switch-thumb" />
</BitBhSwitch>/* All styles are consumer-authored — the library ships zero CSS */
.switch {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Track */
position: relative;
display: inline-block;
width: 2.5rem;
height: 1.4rem;
border-radius: 999px;
background: #d4d4d8;
cursor: pointer;
transition: background 150ms ease;
/* Focus ring */
outline: none;
}
/* When rendered with As="div" (or any non-native element) BitBhSwitch automatically
adds tabindex="0" and a Space/Enter keydown handler — the focus ring still
needs your :focus-visible rule to appear. */
.switch:focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2563eb;
}
/* Hooks emitted by BitBhSwitch */
.switch[data-state="checked"] { background: #2563eb; }
.switch[data-disabled] { opacity: 0.5; cursor: not-allowed; }
/* Thumb — driven by data-state on the inner element */
.switch-thumb {
position: absolute;
top: 50%;
left: 0.15rem;
width: 1.1rem;
height: 1.1rem;
background: #fff;
border-radius: 999px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transform: translateY(-50%);
transition: left 150ms ease;
}
.switch-thumb[data-state="checked"] {
left: calc(100% - 1.1rem - 0.15rem);
}Form Submission
Set Name to render an adjacent <input type="hidden">
so the switch participates in plain form posts. The hidden input is only present
when the switch is checked, mirroring native checkbox behaviour.
<form method="post">
<label class="switch-label">
<BitBhSwitch Name="newsletter" Value="yes"
DefaultChecked="false"
class="switch">
<BitBhSwitchThumb class="switch-thumb" />
</BitBhSwitch>
<span>Subscribe to newsletter</span>
</label>
<button type="submit">Submit</button>
</form>
@* When checked, posts: newsletter=yes *@/* All styles are consumer-authored — the library ships zero CSS */
.switch {
/* Reset native button */
all: unset;
box-sizing: border-box;
/* Track */
position: relative;
display: inline-block;
width: 2.5rem;
height: 1.4rem;
border-radius: 999px;
background: #d4d4d8;
cursor: pointer;
transition: background 150ms ease;
/* Focus ring */
outline: none;
}
.switch:focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2563eb;
}
/* Hooks emitted by BitBhSwitch */
.switch[data-state="checked"] { background: #2563eb; }
.switch[data-disabled] { opacity: 0.5; cursor: not-allowed; }
/* Thumb — driven by data-state on the inner element */
.switch-thumb {
position: absolute;
top: 50%;
left: 0.15rem;
width: 1.1rem;
height: 1.1rem;
background: #fff;
border-radius: 999px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transform: translateY(-50%);
transition: left 150ms ease;
}
.switch-thumb[data-state="checked"] {
left: calc(100% - 1.1rem - 0.15rem);
}
/* Wrapper that aligns the switch with its text label */
.switch-label {
display: inline-flex;
align-items: center;
gap: 0.6rem;
cursor: pointer;
user-select: none;
font-size: 0.9rem;
}
/* When Name is set BitBhSwitch renders an <input type="hidden"> sibling that is
always invisible by default — no extra CSS needed for that. */
.demo-output { font-size: 0.875rem; color: #71717a; }
.btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.45rem 0.85rem;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
background: #fff;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
}
.btn:hover { background: #f4f4f5; }
.btn-primary {
color: #fff;
background: #2563eb;
border-color: #2563eb;
}
.btn-primary:hover {
background: #1d4ed8;
border-color: #1d4ed8;
}