Field / Input
Form primitives with auto-wired ARIA labels, descriptions, and validation state.
Basic Field
Wrap a label, input, and description in an BitBhField. The label's
for, the input's id, and the description's
aria-describedby are all wired automatically — no manual id management.
We'll never share your email.
<BitBhField class="field">
<BitBhLabel class="field-label">Email</BitBhLabel>
<BitBhInput type="email" placeholder="[email protected]" class="field-input" />
<BitBhDescription class="field-description">We'll never share your email.</BitBhDescription>
</BitBhField>
@* Rendered HTML:
<label for="h-xxxx-input" id="h-xxxx-label">Email</label>
<input id="h-xxxx-input" type="email" placeholder="[email protected]"
aria-describedby="h-xxxx-description" />
<p id="h-xxxx-description">We'll never share your email.</p>
*@/* All styles are consumer-authored — the library ships zero CSS */
.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
width: 100%;
max-width: 24rem;
}
.field-label {
font-size: 0.85rem;
font-weight: 600;
color: #18181b;
}
.field-label[data-disabled] { opacity: 0.5; }
.field-input {
padding: 0.5rem 0.75rem;
border: 1px solid #d4d4d8;
border-radius: 0.4rem;
font-size: 0.9rem;
font-family: inherit;
color: #18181b;
background: #fff;
transition: border-color 120ms ease;
}
.field-input:hover { border-color: #a1a1af; }
.field-input:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
/* Hooks emitted by BitBhInput / BitBhTextarea / BitBhSelect */
.field-input[data-disabled] { opacity: 0.5; cursor: not-allowed; background: #fafafa; }
.field-input[data-invalid] { border-color: #dc2626; }
.field-input[data-invalid]:focus-visible { outline-color: #dc2626; }
.field-description {
font-size: 0.8rem;
color: #71717a;
margin: 0;
}
.field-error { color: #dc2626; }Textarea
BitBhTextarea works identically to BitBhInput — same ARIA
wiring, same data attributes. Standard HTML attributes like rows
pass through.
Max 280 characters.
<BitBhField class="field">
<BitBhLabel class="field-label">Bio</BitBhLabel>
<BitBhTextarea rows="3" placeholder="Tell us about yourself…" class="field-input" />
<BitBhDescription class="field-description">Max 280 characters.</BitBhDescription>
</BitBhField>/* All styles are consumer-authored — the library ships zero CSS */
.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
width: 100%;
max-width: 24rem;
}
.field-label {
font-size: 0.85rem;
font-weight: 600;
color: #18181b;
}
.field-label[data-disabled] { opacity: 0.5; }
.field-input {
padding: 0.5rem 0.75rem;
border: 1px solid #d4d4d8;
border-radius: 0.4rem;
font-size: 0.9rem;
font-family: inherit;
color: #18181b;
background: #fff;
transition: border-color 120ms ease;
}
.field-input:hover { border-color: #a1a1af; }
.field-input:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
/* Hooks emitted by BitBhInput / BitBhTextarea / BitBhSelect */
.field-input[data-disabled] { opacity: 0.5; cursor: not-allowed; background: #fafafa; }
.field-input[data-invalid] { border-color: #dc2626; }
.field-input[data-invalid]:focus-visible { outline-color: #dc2626; }
.field-description {
font-size: 0.8rem;
color: #71717a;
margin: 0;
}
.field-error { color: #dc2626; }
/* The textarea just needs a resize rule. */
.field-textarea { resize: vertical; min-height: 4rem; }Select
BitBhSelect renders a native <select> with the same
auto-wired ARIA. Place <option> elements inside.
Where you're based.
<BitBhField class="field">
<BitBhLabel class="field-label">Country</BitBhLabel>
<BitBhSelect class="field-input field-select">
<option value="">Choose…</option>
<option value="us">United States</option>
<option value="gb">United Kingdom</option>
</BitBhSelect>
<BitBhDescription class="field-description">Where you're based.</BitBhDescription>
</BitBhField>/* All styles are consumer-authored — the library ships zero CSS */
.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
width: 100%;
max-width: 24rem;
}
.field-label {
font-size: 0.85rem;
font-weight: 600;
color: #18181b;
}
.field-label[data-disabled] { opacity: 0.5; }
.field-input {
padding: 0.5rem 0.75rem;
border: 1px solid #d4d4d8;
border-radius: 0.4rem;
font-size: 0.9rem;
font-family: inherit;
color: #18181b;
background: #fff;
transition: border-color 120ms ease;
}
.field-input:hover { border-color: #a1a1af; }
.field-input:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
/* Hooks emitted by BitBhInput / BitBhTextarea / BitBhSelect */
.field-input[data-disabled] { opacity: 0.5; cursor: not-allowed; background: #fafafa; }
.field-input[data-invalid] { border-color: #dc2626; }
.field-input[data-invalid]:focus-visible { outline-color: #dc2626; }
.field-description {
font-size: 0.8rem;
color: #71717a;
margin: 0;
}
.field-error { color: #dc2626; }
/* The select needs a small appearance override. */
.field-select { appearance: none; padding-right: 2rem; }Disabled Field
Set Disabled="true" on the BitBhField to disable all
children at once. The field, label, and input all receive data-disabled
for CSS hooks.
Cannot be changed.
<BitBhField Disabled="true" class="field">
<BitBhLabel class="field-label">Username</BitBhLabel>
<BitBhInput type="text" value="alice" class="field-input" />
<BitBhDescription class="field-description">Cannot be changed.</BitBhDescription>
</BitBhField>
@* All children receive data-disabled for CSS hooks. *@/* All styles are consumer-authored — the library ships zero CSS */
.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
width: 100%;
max-width: 24rem;
}
.field-label {
font-size: 0.85rem;
font-weight: 600;
color: #18181b;
}
.field-label[data-disabled] { opacity: 0.5; }
.field-input {
padding: 0.5rem 0.75rem;
border: 1px solid #d4d4d8;
border-radius: 0.4rem;
font-size: 0.9rem;
font-family: inherit;
color: #18181b;
background: #fff;
transition: border-color 120ms ease;
}
.field-input:hover { border-color: #a1a1af; }
.field-input:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
/* Hooks emitted by BitBhInput / BitBhTextarea / BitBhSelect */
.field-input[data-disabled] { opacity: 0.5; cursor: not-allowed; background: #fafafa; }
.field-input[data-invalid] { border-color: #dc2626; }
.field-input[data-invalid]:focus-visible { outline-color: #dc2626; }
.field-description {
font-size: 0.8rem;
color: #71717a;
margin: 0;
}
.field-error { color: #dc2626; }
/* The whole field dims when Disabled="true" is set on BitBhField */
.field[data-disabled] .field-input { opacity: 0.6; }Invalid Field
Set Invalid="true" on the BitBhField to mark the input
as invalid. The input receives aria-invalid="true" and
data-invalid for CSS hooks. Use the description to show an error message.
Password must be at least 8 characters.
<BitBhField Invalid="true" class="field">
<BitBhLabel class="field-label">Password</BitBhLabel>
<BitBhInput type="password" value="123" class="field-input" />
<BitBhDescription class="field-description field-error">
Password must be at least 8 characters.
</BitBhDescription>
</BitBhField>
@* The input receives aria-invalid="true" and data-invalid. *@/* All styles are consumer-authored — the library ships zero CSS */
.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
width: 100%;
max-width: 24rem;
}
.field-label {
font-size: 0.85rem;
font-weight: 600;
color: #18181b;
}
.field-label[data-disabled] { opacity: 0.5; }
.field-input {
padding: 0.5rem 0.75rem;
border: 1px solid #d4d4d8;
border-radius: 0.4rem;
font-size: 0.9rem;
font-family: inherit;
color: #18181b;
background: #fff;
transition: border-color 120ms ease;
}
.field-input:hover { border-color: #a1a1af; }
.field-input:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
/* Hooks emitted by BitBhInput / BitBhTextarea / BitBhSelect */
.field-input[data-disabled] { opacity: 0.5; cursor: not-allowed; background: #fafafa; }
.field-input[data-invalid] { border-color: #dc2626; }
.field-input[data-invalid]:focus-visible { outline-color: #dc2626; }
.field-description {
font-size: 0.8rem;
color: #71717a;
margin: 0;
}
.field-error { color: #dc2626; }Multiple Fields (form layout)
Compose multiple BitBhField components into a form. Each field
manages its own id namespace — no collisions.
<form class="field-form">
<BitBhField class="field">
<BitBhLabel class="field-label">First name</BitBhLabel>
<BitBhInput type="text" placeholder="Alice" class="field-input" />
</BitBhField>
<BitBhField class="field">
<BitBhLabel class="field-label">Last name</BitBhLabel>
<BitBhInput type="text" placeholder="Anderson" class="field-input" />
</BitBhField>
<BitBhField class="field">
<BitBhLabel class="field-label">Email</BitBhLabel>
<BitBhInput type="email" placeholder="[email protected]" class="field-input" />
<BitBhDescription class="field-description">Your work email.</BitBhDescription>
</BitBhField>
<button type="submit" class="btn btn-primary">Submit</button>
</form>/* All styles are consumer-authored — the library ships zero CSS */
.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
width: 100%;
max-width: 24rem;
}
.field-label {
font-size: 0.85rem;
font-weight: 600;
color: #18181b;
}
.field-label[data-disabled] { opacity: 0.5; }
.field-input {
padding: 0.5rem 0.75rem;
border: 1px solid #d4d4d8;
border-radius: 0.4rem;
font-size: 0.9rem;
font-family: inherit;
color: #18181b;
background: #fff;
transition: border-color 120ms ease;
}
.field-input:hover { border-color: #a1a1af; }
.field-input:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
/* Hooks emitted by BitBhInput / BitBhTextarea / BitBhSelect */
.field-input[data-disabled] { opacity: 0.5; cursor: not-allowed; background: #fafafa; }
.field-input[data-invalid] { border-color: #dc2626; }
.field-input[data-invalid]:focus-visible { outline-color: #dc2626; }
.field-description {
font-size: 0.8rem;
color: #71717a;
margin: 0;
}
.field-error { color: #dc2626; }
/* Simple vertical form layout */
.field-form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 24rem;
}
/* Submit button */
.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; }EditForm validation
Inside an EditForm with DataAnnotationsValidator, every control bound
with @bind-Value / @bind-Checked reports its validation state to its
BitBhField: the control gets aria-invalid="true" and
data-invalid, and BitBhErrorMessage appears with the messages and is
added to the control's aria-describedby — no ids to wire. Non-native controls work
too: the radio group is named by its BitBhLabel via aria-labelledby, and
clicking the label focuses the group. Because these components take plain child content, they
nest inside EditForm without Context clashes. Submit empty to see it.
@using System.ComponentModel.DataAnnotations
@* novalidate: let DataAnnotations (not the browser) report errors *@
<EditForm Model="signup" OnValidSubmit="HandleValidSubmit" novalidate="@true" class="field-form validation-form">
<DataAnnotationsValidator />
<BitBhField Required="true" class="field">
<BitBhLabel class="field-label">Full name</BitBhLabel>
<BitBhInput @bind-Value="signup.Name" autocomplete="name" class="field-input" />
<BitBhErrorMessage class="validation-message" /> @* only rendered while invalid *@
</BitBhField>
<BitBhField Required="true" class="field">
<BitBhLabel class="field-label">Email</BitBhLabel>
<BitBhDescription class="field-description">We'll send the receipt here.</BitBhDescription>
<BitBhInput type="email" @bind-Value="signup.Email" autocomplete="email" class="field-input" />
<BitBhErrorMessage class="validation-message" />
</BitBhField>
@* Non-native control: labelled via aria-labelledby, focused by a label click *@
<BitBhField Required="true" class="field">
<BitBhLabel class="field-label">Plan</BitBhLabel>
<BitBhRadioGroup @bind-Value="signup.Plan" Vertical="false" class="validation-radios">
<BitBhRadio Value="Free" class="validation-radio"><span class="validation-radio-dot" aria-hidden="true"></span>Free</BitBhRadio>
<BitBhRadio Value="Pro" class="validation-radio"><span class="validation-radio-dot" aria-hidden="true"></span>Pro</BitBhRadio>
<BitBhRadio Value="Team" class="validation-radio"><span class="validation-radio-dot" aria-hidden="true"></span>Team</BitBhRadio>
</BitBhRadioGroup>
<BitBhErrorMessage class="validation-message" />
</BitBhField>
<BitBhField class="field">
<div class="validation-check">
<BitBhCheckbox @bind-Checked="signup.AcceptTerms" class="checkbox">
<BitBhCheckboxIndicator class="checkbox-tick">✓</BitBhCheckboxIndicator>
</BitBhCheckbox>
<BitBhLabel class="validation-check-label">I accept the terms of service</BitBhLabel>
</div>
<BitBhErrorMessage class="validation-message" />
</BitBhField>
<div class="validation-actions">
<button type="submit" class="btn btn-primary">Create account</button>
<button type="button" class="btn" @onclick="ResetSignup">Reset</button>
</div>
<BitBhLiveRegion class="validation-success">@signupResult</BitBhLiveRegion>
</EditForm>
@code {
private sealed class SignupModel
{
[Required(ErrorMessage = "Please enter your name.")]
public string? Name { get; set; }
[Required(ErrorMessage = "Please enter your email.")]
[EmailAddress(ErrorMessage = "That doesn't look like an email address.")]
public string? Email { get; set; }
[Required(ErrorMessage = "Pick a plan.")]
public string? Plan { get; set; }
[Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the terms.")]
public bool AcceptTerms { get; set; }
}
private SignupModel signup = new();
private string? signupResult;
private void HandleValidSubmit() =>
signupResult = $"Welcome aboard, {signup.Name}! You're on the {signup.Plan} plan.";
private void ResetSignup()
{
signup = new();
signupResult = null;
}
}/* All styles are consumer-authored — the library ships zero CSS.
Builds on .field / .field-label / .field-input / .field-description from
the "Basic Field" demo and .checkbox from the Checkbox page. */
.validation-form { display: flex; flex-direction: column; gap: 1rem; max-width: 24rem; }
/* BitBhLabel emits data-required / data-invalid from its field */
.validation-form .field-label[data-required]::after { content: " *"; color: #dc2626; }
.validation-form .field-label[data-invalid] { color: #b91c1c; }
/* BitBhInput: aria-invalid="true" + data-invalid */
.field-input[data-invalid] { border-color: #dc2626; }
.field-input[data-invalid]:focus-visible { outline-color: #dc2626; }
/* BitBhErrorMessage: rendered only while the field is invalid */
.validation-message {
display: flex;
align-items: center;
gap: 0.35rem;
margin: 0;
color: #dc2626;
font-size: 0.8rem;
font-weight: 500;
}
.validation-message::before {
content: "!";
display: inline-flex;
align-items: center;
justify-content: center;
width: 0.95rem;
height: 0.95rem;
border-radius: 999px;
background: #dc2626;
color: #fff;
font-size: 0.65rem;
font-weight: 700;
}
/* Radio group as pills — the radio's text is its accessible name */
.validation-radios { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.validation-radio {
all: unset;
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.85rem;
border: 1px solid #d4d4d8;
border-radius: 0.5rem;
background: #fff;
font-size: 0.875rem;
cursor: pointer;
}
.validation-radio:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.validation-radio[data-state="checked"] { border-color: #2563eb; background: #eff6ff; }
.validation-radios[aria-invalid="true"] .validation-radio { border-color: #fca5a5; }
.validation-radio-dot {
width: 0.9rem;
height: 0.9rem;
box-sizing: border-box;
border: 1px solid #a1a1aa;
border-radius: 999px;
background: #fff;
}
.validation-radio[data-state="checked"] .validation-radio-dot { border: 4px solid #2563eb; }
/* Checkbox + label row */
.validation-check { display: flex; align-items: center; gap: 0.55rem; }
.validation-check-label { font-size: 0.875rem; cursor: pointer; }
.validation-form .checkbox[data-invalid] { border-color: #dc2626; }
.validation-actions { display: flex; gap: 0.5rem; }
/* The live region stays rendered; style it only when it has content */
.validation-success:not(:empty) {
padding: 0.6rem 0.85rem;
border: 1px solid #bbf7d0;
border-radius: 0.5rem;
background: #f0fdf4;
color: #166534;
font-size: 0.875rem;
}