Button
A polymorphic, accessible button primitive with loading and disabled states.
Basic Button
Renders a native <button> with no styling. All visuals come from you.
<BitBhButton OnClick="HandleBasicClick" class="btn">
Click me
</BitBhButton>
<span class="demo-output">Clicked @basicClickCount times</span>
@code {
private int basicClickCount;
private void HandleBasicClick() => basicClickCount++;
}.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; }
.demo-output {
font-size: 0.875rem;
color: #71717a;
}Disabled State
Sets the native disabled attribute and emits data-disabled for CSS hooks.
<BitBhButton Disabled="true" OnClick="HandleBasicClick" class="btn">
Can't click me
</BitBhButton>
@code {
private int basicClickCount;
private void HandleBasicClick() => basicClickCount++;
}.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; }
/* Hooks emitted by BitBhButton when Disabled="true" */
.btn[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}Loading State with Context
Uses RenderFragment<BitBhButtonContext> so you can conditionally render
a spinner driven by the button's own state.
<BitBhButton Loading="@isLoading" OnClick="HandleLoadingClick" class="btn">
<Template Context="btn">
@if (btn.Loading)
{
<span class="spinner"></span>
<span>Saving…</span>
}
else
{
<span>Save Changes</span>
}
</Template>
</BitBhButton>
@code {
private bool isLoading;
private async Task HandleLoadingClick()
{
isLoading = true;
await Task.Delay(2000);
isLoading = false;
}
}.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; }
/* Hooks emitted by BitBhButton when Loading="true" */
.btn[data-loading] {
opacity: 0.7;
cursor: wait;
}
.spinner {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }Polymorphic Rendering
Set As="a" to render as an anchor while retaining all button semantics.
Non-native elements automatically receive role="button", tabindex="0",
and Enter/Space keyboard handling.
<BitBhButton As="a" href="https://github.com/bitfoundation/bitplatform/tree/develop/src/Utilities/Bheadless" target="_blank" class="btn btn-link">
I'm actually an <a> tag
</BitBhButton>
<BitBhButton As="span" OnClick="HandleBasicClick" class="btn">
I'm a <span> with role="button"
</BitBhButton>
@code {
private int basicClickCount;
private void HandleBasicClick() => basicClickCount++;
}.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; }
/* Anchor variant of BitBhButton (As="a") */
.btn-link {
background: transparent;
border-color: transparent;
color: #2563eb;
text-decoration: underline;
text-underline-offset: 2px;
}
.btn-link:hover { color: #1d4ed8; background: #eff6ff; }Toggle Button
Combine BitBhButton with your own state to build a toggle. Pass
aria-pressed via standard HTML attributes — they merge automatically.
<BitBhButton OnClick="TogglePressed"
class="@ToggleClass"
aria-pressed="@isPressed.ToString().ToLowerInvariant()">
@(isPressed ? "ON" : "OFF")
</BitBhButton>
@code {
private bool isPressed;
private string ToggleClass => isPressed ? "btn btn-toggle active" : "btn btn-toggle";
private void TogglePressed() => isPressed = !isPressed;
}.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; }
/* Toggle button — driven by your own aria-pressed state */
.btn-toggle {
min-width: 4rem;
justify-content: center;
}
.btn-toggle.active {
background: #2563eb;
border-color: #2563eb;
color: white;
}Styled via Data Attributes
Style entirely via CSS attribute selectors. No component API for colors, sizes, or variants — that's your domain.
<BitBhButton OnClick="HandleBasicClick" class="btn-data">
Styled via [data-*]
</BitBhButton>
<BitBhButton Disabled="true" class="btn-data">
Disabled via [data-disabled]
</BitBhButton>
<BitBhButton Loading="true" class="btn-data">
Loading via [data-loading]
</BitBhButton>
@code {
private int basicClickCount;
private void HandleBasicClick() => basicClickCount++;
}/* Style entirely via CSS attribute selectors emitted by BitBhButton */
.btn-data { background: #18181b; color: white; }
.btn-data[data-disabled] { opacity: 0.4; cursor: not-allowed; }
.btn-data[data-loading] { opacity: 0.7; cursor: wait; }