Switch
A polished boolean toggle primitive with built-in track and thumb styling. Renders
beautifully by default and exposes data-state attributes for CSS-only state targeting.
The InputClass and TrackClass parameters have been removed.
MSwitch now ships with a polished default track and thumb — no manual styling required.
Before:
<MSwitch InputClass="peer sr-only"
TrackClass="relative block w-11 h-6 rounded-full ..."
Class="inline-flex items-center gap-2 cursor-pointer">
Enable feature
</MSwitch>After:
<MSwitch>
Enable feature
</MSwitch>
Use Class to customize the wrapper layout (gap, flex direction). Use
--marai-primary CSS token override to change the track color globally.
Use data-[state=checked]: Tailwind utilities in your own CSS for per-instance overrides.
Overview
MSwitch renders a root <label> wrapping a visually-hidden
<input type="checkbox" role="switch">, a styled track span, and a thumb span.
The track and thumb use data-state attributes ("checked" / "unchecked")
to apply their visual state entirely through Tailwind's data-[state=checked]:* utilities —
no JavaScript state management needed. The Class parameter targets the root wrapper only.
Usage
Add the namespace import to use the component.
@using Marai.UI.Components.MSwitchBasic Usage
No parameters needed — MSwitch renders styled out of the box:
@using Marai.UI.Components.MSwitch
<MSwitch aria-label="Toggle feature" />
Layout Customization
Use Class to adjust the wrapper layout — gap, flex direction, or alignment.
The track and thumb are always styled by the component's defaults:
@using Marai.UI.Components.MSwitch
<div class="flex flex-col gap-3">
<MSwitch>
Default — styled out of the box
</MSwitch>
<MSwitch Class="inline-flex flex-row-reverse items-center gap-2 cursor-pointer">
Track on left, label on right
</MSwitch>
</div>
Examples
Two-Way Binding
Use @bind-Checked to keep a field in sync with the toggle state.
State: Off
@using Marai.UI.Components.MSwitch
<div class="flex flex-col gap-3">
<MSwitch @bind-Checked="_enabled">
Airplane mode
</MSwitch>
<p class="text-sm text-muted-foreground">State: @(_enabled ? "On" : "Off")</p>
</div>
@code {
private bool _enabled;
}
With Label
Place label text directly inside MSwitch as ChildContent for an
inline label, or pair with an external MLabel using a shared id.
@using Marai.UI.Components.MSwitch
@using Marai.UI.Components.MLabel
<div class="flex flex-col gap-3">
<MSwitch>
Enable notifications
</MSwitch>
<div class="flex items-center gap-2">
<MSwitch id="dark-mode" aria-label="Dark mode" />
<MLabel For="dark-mode">Dark mode</MLabel>
</div>
</div>
Disabled (Off)
The Disabled parameter applies the native disabled attribute. Default styling applies opacity-50 and cursor-not-allowed automatically.
@using Marai.UI.Components.MSwitch
<MSwitch Disabled="true">
Disabled (off)
</MSwitch>
Disabled (On)
A disabled switch in the checked state shows the on appearance without allowing changes.
@using Marai.UI.Components.MSwitch
<MSwitch Checked="true" Disabled="true">
Disabled (on)
</MSwitch>
In a Form
Use @bind-Checked inside an EditForm for model binding and validation support.
Supply a CheckedExpression when you need <ValidationMessage> to identify the field.
With @bind-Checked this is handled automatically by the Blazor compiler.
<EditForm Model="_model" OnValidSubmit="HandleSubmit">
<DataAnnotationsValidator />
<div class="flex flex-col gap-2">
<MSwitch id="notifications"
@bind-Checked="_model.NotificationsEnabled"
Class="inline-flex items-center gap-2 cursor-pointer"
InputClass="peer sr-only"
TrackClass="relative block w-11 h-6 rounded-full bg-muted transition-colors after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:h-5 after:w-5 after:rounded-full after:bg-white after:transition-all peer-checked:after:translate-x-5 peer-checked:bg-primary peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2">
Enable notifications
</MSwitch>
<ValidationMessage For="() => _model.NotificationsEnabled" />
</div>
<MButton Type="submit" Class="mt-4">Save</MButton>
</EditForm>
@code {
private readonly MyModel _model = new();
private void HandleSubmit() { /* ... */ }
}
Standard HTML Attributes
Unmatched attributes are forwarded to the underlying <input>.
Pass id, name, aria-describedby, and any other HTML attribute directly on the component.
Receive updates about your account activity.
@using Marai.UI.Components.MSwitch
<div class="flex flex-col gap-2">
<MSwitch id="notifications"
name="notifications"
aria-describedby="notifications-hint"
@bind-Checked="_enabled">
Email notifications
</MSwitch>
<p id="notifications-hint" class="text-sm text-muted-foreground">
Receive updates about your account activity.
</p>
</div>
@code {
private bool _enabled;
}
Accessibility
- The native
<input type="checkbox">carriesrole="switch"for correct semantics - Keyboard accessible — Space toggles the switch when focused
- The input is visually hidden (
sr-only) but stays in the accessibility tree - Always provide a label: use
ChildContentfor inline text, or an externalMLabelwith a matchingid - Use
aria-labelwhen no visible label is present (e.g., standalone icon toggles) - Use
aria-describedbyto link help text or validation messages to the switch - The
data-state="checked|unchecked"attribute reflects toggle state for CSS or test targeting - The
data-disabled="true|false"attribute reflects disabled state for CSS or test targeting
API Reference
MSwitch
| Parameter | Type | Default | Description |
|---|---|---|---|
| Checked | bool | false |
The current toggle state. Use with @bind-Checked for two-way binding. |
| CheckedChanged | EventCallback<bool> | — |
Callback invoked when the toggle state changes. Wired automatically by @bind-Checked. |
| CheckedExpression | Expression<Func<bool>>? | null |
Expression identifying the bound field for <ValidationMessage>. Set automatically by @bind-Checked. |
| Disabled | bool | false |
Applies the native disabled attribute. Default styling applies opacity and cursor automatically. |
| ChildContent | RenderFragment? | null |
Optional label text or content rendered inside the root label, alongside the track. |
| Class | string? | null |
Additional CSS classes applied to the root <label> wrapper. Defaults to inline-flex items-center gap-2 cursor-pointer. |
| AdditionalAttributes | Dictionary<string, object>? | null |
Arbitrary HTML attributes forwarded to the <input> element (e.g. aria-label, id, name). |
Styling Guide
data-state Attributes
The root <label>, track <span>, and thumb <span>
all expose data-state="checked|unchecked". Target them in your own CSS for per-instance
overrides beyond what the wrapper Class provides:
/* Target the checked track within a specific switch */
.my-custom-switch [data-state=checked] {
background-color: hsl(var(--marai-success));
}Global Track Color
Override the --marai-primary CSS token to change the checked track color for all switches
at once — no per-component changes needed:
:root {
/* All switches and primary buttons update at once */
--marai-primary: 142 76% 36%;
--marai-primary-foreground: 0 0% 100%;
}- Always provide a visible label — use
ChildContentor an externalMLabel - Use
@bind-Checkedfor reactive state rather than settingCheckedmanually - Add
aria-describedbywhen help text or validation messages accompany the switch - Group related switches in a
fieldsetwith alegendfor screen reader context - Override
--marai-primaryin your theme CSS for brand-consistent toggle colors