Checkbox
Live Demo
Open the demo in a separate tab.
Model binding:
CheckboxConfig::new().label("Active").bind(User::active)connects a boolean model field to the checkbox. Configure first, then bind; the result is a composableUiElement. See Binding Controls.
checkbox() is the standard Beverly form element for boolean values.
Like every Beverly form element, a checkbox requires:
- A label
- An ARIA declaration
- A Model binding
#![allow(unused)]
fn main() {
checkbox()
.label(localize("user.active.label"))
.aria(localize("user.active.description"))
.bind(User::active)
}
The checkbox does not own the boolean value.
The Model does.
Binding
A checkbox binds directly to a boolean Model property:
#![allow(unused)]
fn main() {
checkbox()
.label(localize("user.active.label"))
.aria(localize("user.active.description"))
.bind(User::active)
}
The binding works in both directions:
Model → Binding → Checkbox
Checkbox → Binding → Setter → Model
If the Model contains:
#![allow(unused)]
fn main() {
active: true
}
the checkbox is checked.
If the user unchecks it, the binding sends false to the Model.
If another part of the application changes active, the checkbox reflects the new Model state.
The Setter Is Called on Every Change
Just like input() and textarea(), the setter is invoked whenever the checkbox changes.
Checked
↓
true
↓
Setter
Unchecked
↓
false
↓
Setter
For a simple boolean, the setter may be straightforward:
#![allow(unused)]
fn main() {
impl User {
fn set_active(
&mut self,
active: bool,
) -> Result<(), UserError> {
self.active = active;
Ok(())
}
}
}
The Model can still enforce domain rules.
For example, perhaps a user cannot deactivate themselves while they are the only administrator:
#![allow(unused)]
fn main() {
impl User {
fn set_active(
&mut self,
active: bool,
) -> Result<(), UserError> {
if !active && self.is_only_admin() {
return Err(UserError::CannotDeactivateOnlyAdmin);
}
self.active = active;
Ok(())
}
}
}
The checkbox does not need to understand that rule.
The Model does.
The Model Can Reject a Change
Suppose the user attempts to uncheck the checkbox.
The binding invokes the setter:
Checkbox
↓
Binding
↓
User::set_active(false)
↓
Err(UserError::CannotDeactivateOnlyAdmin)
The Model rejects the state transition.
The existing Model value remains authoritative.
The binding receives the error and exposes it to the checkbox’s error context.
#![allow(unused)]
fn main() {
checkbox()
.label(localize("user.active.label"))
.aria(localize("user.active.description"))
.bind(User::active)
.error(|errors| text(errors.join("\n")))
}
The errors are derived from the binding.
The checkbox does not invent them.
The Model decides whether the checkbox can change. The checkbox presents the result.
Error Context
Just like the text input and textarea, the checkbox’s .error(...) callback
receives &[String] from the current validation state and returns visible
content below the control. See Error Callbacks.
#![allow(unused)]
fn main() {
.error(|errors| text(errors.join("\n")))
}
Two things are automatically available inside the state hook:
self— the checkbox itselferrors— the current field errors as&[String]
The Model supplies the errors.
The binding associates them with the checkbox.
The View determines how they are presented.
Internationalization
Validation messages should not be hard-coded into the checkbox.
Avoid:
#![allow(unused)]
fn main() {
text("You cannot deactivate yourself.")
}
Instead, the Model returns a structured error and the error context exposes the localized message.
#![allow(unused)]
fn main() {
checkbox()
.label(localize("user.active.label"))
.aria(localize("user.active.description"))
.bind(User::active)
.error(|errors| text(errors.join("\n")))
}
The label and ARIA description come from localization.
The validation error comes from the Model.
This means the checkbox contains no language-specific validation logic.
The Model returns meaning. The UI presents language.
The Model Can Transform State
A checkbox normally represents a boolean directly, but the Model can still perform additional work when the value changes.
For example, changing active might update related application state:
#![allow(unused)]
fn main() {
impl User {
fn set_active(
&mut self,
active: bool,
) -> Result<(), UserError> {
self.active = active;
self.update_access_state();
Ok(())
}
}
}
The checkbox does not need to know what update_access_state() does.
It simply reflects the resulting Model state.
This is the same principle as phone-number formatting with input().
The form element reports intent. The Model determines the resulting state.
Asynchronous Changes
A checkbox can also trigger an asynchronous Model transition.
For example, activating an account might require a remote operation:
#![allow(unused)]
fn main() {
impl User {
async fn set_active(
&mut self,
active: bool,
) -> Result<(), UserError> {
update_account_status(active).await?;
self.active = active;
Ok(())
}
}
}
The checkbox can represent the pending state:
#![allow(unused)]
fn main() {
checkbox()
.label(localize("user.active.label"))
.aria(localize("user.active.description"))
.bind(User::active)
.submitting(|| [spinner()])
}
Or:
#![allow(unused)]
fn main() {
checkbox()
.label(localize("user.active.label"))
.aria(localize("user.active.description"))
.bind(User::active)
.submitting(|| [
spinner(),
text(localize("user.active.updating"))
])
}
Again, self refers to the checkbox.
The async operation does not directly manipulate the component.
The application sets FormStatusState::submitting() and sets idle() or
success() when the operation changes state. The callback supplies content
below the checkbox; it does not style or disable the checkbox.
No Manual Change Handler
A checkbox should not require code like:
#![allow(unused)]
fn main() {
on_change(|checked| {
// manually update the Model
})
}
The binding already establishes that relationship.
Instead:
#![allow(unused)]
fn main() {
checkbox()
.label(localize("user.active.label"))
.aria(localize("user.active.description"))
.bind(User::active)
}
is enough.
The framework knows:
- The checkbox represents
User::active. - The current Model value determines whether it is checked.
- A user change invokes the corresponding setter.
- The setter can accept, reject, transform, or asynchronously process the change.
- The resulting Model state is reflected back into the checkbox.
Styling
Checkboxes use the same fluent component API:
#![allow(unused)]
fn main() {
checkbox()
.label(localize("user.active.label"))
.aria(localize("user.active.description"))
.bind(User::active)
.gap(8)
}
Error presentation:
#![allow(unused)]
fn main() {
checkbox()
.label(localize("user.active.label"))
.aria(localize("user.active.description"))
.bind(User::active)
.error(|errors| text(errors.join("\n")))
}
Pending presentation:
#![allow(unused)]
fn main() {
checkbox()
.label(localize("user.active.label"))
.aria(localize("user.active.description"))
.bind(User::active)
.submitting(|| [spinner()])
}
The same API works for normal, error, and pending states.
The Complete Checkbox
A production checkbox might look like:
#![allow(unused)]
fn main() {
checkbox()
.label(localize("user.active.label"))
.aria(localize("user.active.description"))
.bind(User::active)
.gap(8)
.error(|errors| text(errors.join("\n")))
.submitting(|| [spinner()])
}
The checkbox has:
- A required label
- A required ARIA declaration
- A typed
boolbinding - Two-way Model synchronization
- Model-owned validation
- Model-owned state transitions
- Model-owned asynchronous behavior
- Automatic error propagation
- Localized error presentation
- Pending-state presentation
- Fluent styling
The checkbox itself remains simple.
The checkbox represents a boolean. The binding connects it. The setter decides whether that boolean can change. The Model owns the state.