ee8797c142
Co-authored-by: Copilot <copilot@github.com>
6.6 KiB
6.6 KiB
Switch
A toggle switch (on/off). Renders as a hidden <input type="checkbox"> with a styled track and thumb driven by JavaScript. Fires no custom events — read the underlying checkbox value in form submissions.
HTML structure
label[for={id}].flex.items-center.gap-3.cursor-pointer
div.switch-root.relative.w-11.h-6.rounded-full ← outer track
input[type=checkbox, id, name, class="sr-only", $$Checked$$] ← hidden; holds true state
div.switch-thumb.absolute.top-0.5.left-0.5... ← animated thumb
span.text-sm.select-none ← label text (omitted when empty)
{label}
CSS mechanics
| Class | Effect |
|---|---|
sr-only |
Hides the real checkbox visually but keeps it accessible |
switch-root |
bg-input (off) / bg-primary (on) — toggled by JS adding switch-on class |
switch-thumb |
h-5 w-5 rounded-full bg-background shadow transition-transform |
translate-x-5 |
Added to thumb by JS when switch is on (slides right) |
JavaScript (initSwitch in components.js)
Runs on DOMContentLoaded and htmx:afterSwap.
Per-switch initialization:
- Guard
_switchInitprevents double-binding - Sync visual state from the hidden checkbox
checkedproperty on load - On
labelclick: togglechecked, toggleswitch-onon the track, toggletranslate-x-5on the thumb
Constructor signature
public Switch(
string id,
string label = "",
string name = "",
bool isChecked = false)
| Parameter | Description |
|---|---|
id |
Element id for the hidden checkbox; label's for attribute |
label |
Optional visible text to the right of the toggle |
name |
Form field name for the hidden checkbox |
isChecked |
Initial on/off state |
Usage examples
Basic on/off toggle
new Switch(
id: "notifications",
label: "Enable notifications",
name: "enableNotifications",
isChecked: true)
Toggle without label
new Switch(id: "darkMode", name: "darkMode")
Reading in a form handler
public record Command(
[property: FromForm] string? EnableNotifications = null
);
bool notificationsOn = command.EnableNotifications != null;
Like all checkboxes, an unchecked switch is not included in the form submission. Use
nullas the default in your command record.
HTMX auto-save on change
// The hidden checkbox is named, so wrap in a form or use hx-include:
new Switch(
id: "maintenance",
name: "maintenanceMode",
label: "Maintenance mode",
isChecked: currentState)
<!-- Parent form with HTMX:
<form hx-post="/settings" hx-trigger="change from:#maintenance">
$$MaintenanceSwitch$$
</form>
-->
Tips and tricks
- The hidden checkbox carries the value
"on"when checked (standard checkbox default). If you need"true", addvalue="true"by subclassing or via a wrapper form. - Because the click is handled on the
<label>element, the switch works correctly even when the hidden input is not directly clicked. - For an HTMX auto-save switch, trigger on
changefrom the hidden checkbox usinghx-trigger="change from:#myId"on a parent element. - Pairing
isCheckedwith a server-read preference ensures the switch reflects the saved state on every page load. - Pairing
isCheckedwith a server-read preference ensures the switch reflects the saved state on every page load.
Complete page example
Templates/NotificationsPage.htmx
<div class="max-w-md mx-auto py-10">
<h1 class="text-2xl font-bold mb-6">Notifications</h1>
<form method="post" action="/notifications">
$$AntiforgeryToken$$
<div class="space-y-5 mb-8">
$$EmailSwitch$$
$$PushSwitch$$
$$SmsSwitch$$
</div>
$$SaveBtn$$
</form>
$$SuccessAlert$$
</div>
Templates/NotificationsPage.htmx.cs
namespace Htmx.ApiDemo.Templates;
public sealed class NotificationsPage : NotificationsPageBase
{
private readonly IHtmxComponent _email;
private readonly IHtmxComponent _push;
private readonly IHtmxComponent _sms;
private readonly IHtmxComponent _save;
private readonly IHtmxComponent _success;
private readonly byte[] _afToken;
public NotificationsPage(
IAntiforgery af,
HttpContext ctx,
NotificationPrefs? prefs = null,
bool saved = false)
{
var tokens = af.GetAndStoreTokens(ctx);
_afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();
_email = new Components.Switch(id: "email-notif", label: "Email notifications", name: "emailNotif", isChecked: prefs?.Email ?? true);
_push = new Components.Switch(id: "push-notif", label: "Push notifications", name: "pushNotif", isChecked: prefs?.Push ?? false);
_sms = new Components.Switch(id: "sms-notif", label: "SMS notifications", name: "smsNotif", isChecked: prefs?.Sms ?? false);
_save = new Components.Button("Save", type: "submit");
_success = saved ? new Components.Alert(title: "Notification preferences saved.") : HtmxEmpty.Instance;
}
protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
protected override void RenderEmailSwitch(HtmxRenderContext ctx) => _email.Render(ctx.Next());
protected override void RenderPushSwitch(HtmxRenderContext ctx) => _push.Render(ctx.Next());
protected override void RenderSmsSwitch(HtmxRenderContext ctx) => _sms.Render(ctx.Next());
protected override void RenderSaveBtn(HtmxRenderContext ctx) => _save.Render(ctx.Next());
protected override void RenderSuccessAlert(HtmxRenderContext ctx) => _success.Render(ctx.Next());
}
POST handler
[Handler]
[MapPost("/notifications")]
public static partial class PostNotificationsHandler
{
public record Command(
[property: FromForm] string? EmailNotif = null,
[property: FromForm] string? PushNotif = null,
[property: FromForm] string? SmsNotif = null);
private static Task<IResult> HandleAsync(
[AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
{
var prefs = new NotificationPrefs(
Email: cmd.EmailNotif != null,
Push: cmd.PushNotif != null,
Sms: cmd.SmsNotif != null);
// Persist prefs…
return ctx.WriteHtmxPage(
new NotificationsPage(af, ctx, prefs, saved: true), title: "Notifications");
}
}
public record NotificationPrefs(bool Email, bool Push, bool Sms);
AppJsonSerializerContext.cs
[JsonSerializable(typeof(PostNotificationsHandler.Command), TypeInfoPropertyName = "NotificationsCommand")]