M
Marai.UI
v1.0.0-alpha.7

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.

Breaking Change — v1.0.0-Alpha.6

The InputClass and TrackClass parameters have been removed. MSwitch now ships with a polished default track and thumb — no manual styling required.

Before:

razor
<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:

razor
<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.

razor
@using Marai.UI.Components.MSwitch

Basic Usage

No parameters needed — MSwitch renders styled out of the box:

Preview
razor
@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:

Preview
razor
@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.

Preview

State: Off

razor
@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.

Preview
razor
@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.

Preview
razor
@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.

Preview
razor
@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.

Note

Supply a CheckedExpression when you need <ValidationMessage> to identify the field. With @bind-Checked this is handled automatically by the Blazor compiler.

razor
<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.

Preview

Receive updates about your account activity.

razor
@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"> carries role="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 ChildContent for inline text, or an external MLabel with a matching id
  • Use aria-label when no visible label is present (e.g., standalone icon toggles)
  • Use aria-describedby to 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:

css
/* 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:

css
:root {
    /* All switches and primary buttons update at once */
    --marai-primary: 142 76% 36%;
    --marai-primary-foreground: 0 0% 100%;
}
Best Practices
  • Always provide a visible label — use ChildContent or an external MLabel
  • Use @bind-Checked for reactive state rather than setting Checked manually
  • Add aria-describedby when help text or validation messages accompany the switch
  • Group related switches in a fieldset with a legend for screen reader context
  • Override --marai-primary in your theme CSS for brand-consistent toggle colors