Switch
A control that turns something on, and says so.
src/components/rahti_ui/switch.rs
Installation
Install this component in your Rahti 0.0.58 application. Class conflicts are resolved with the existing frontend `twMerge`; no Rust class-merging dependency is needed.
Basics
Two switches, both rendered by the server, and the thumb slides on click with no JavaScript involved — it is a background-position that `transition-all` animates, not an element being translated. Clicking either works with JavaScript switched off, which is the whole reason this is an `<input>` and not shadcn's `<button role="switch">`.
Sizes
shadcn's two, kept the way shadcn keeps them: the size is a `data-size` attribute, and the class list reads it. One gradient serves both thumbs — the circle is drawn at 50% of its own tile, so a 16px tile gives an 8px radius and a 12px one gives 6px, and only `bg-[size:…]` has to know which.
States
A `false` leaves the attribute off rather than writing `checked="false"`, which HTML would read as checked. There is no `invalid` prop, and that is fidelity rather than an omission: shadcn's switch carries no `aria-invalid` utilities, so there is nothing here for one to paint.
With a Label
The two ways HTML associates a label with a control, and both make the text clickable. `Label`'s class list is `flex items-center gap-2`, so the first example only has to override `items-center` to line a two-line description up with the switch.
Disabled, and a Label that knows
This is why shadcn's class list opens with `peer`. The Label carries `peer-disabled:cursor-not-allowed peer-disabled:opacity-50`, which fires when a preceding sibling marked `peer` is disabled — and these are plain tags, a Switch and a Label with nothing overridden, each rendering as one literal element, so the switch really is the label's preceding sibling.
A settings list, as a form posts it
What a switch is usually for. Each one has its own `name`, and a checkbox posts nothing at all when it is off — so the server reads an absent key as `false`, which is HTML's rule rather than this component's to restate. Three switches, no array in the page and no JavaScript.
Required, and what it posts
`.value(…)` is what lands in the query string when the switch is on; with none written a checkbox posts `on`, which reads well enough for a switch that it is worth leaving alone. Submit with it off and the browser refuses — `required` on a checkbox means it has to be ticked.
Attributes
`.attr(…)` writes anything; `.unset(…)` takes one away, which is the thing a value cannot express and the reason a forwarded set is an `Attrs` rather than a list of pairs. The second switch has no visible label, so it carries an `aria-label` instead — and `role="switch"` means what a screen reader announces is "Standalone switch, switch, off".
Class override
Rust composes the component and caller class lists. When utilities conflict, use the existing frontend `twMerge` in a compiled class binding.
What this port changed
shadcn's Switch is two elements: a Radix Root that draws the track and a Thumb inside it that slides. This is one element, because the element it is — `<input type="checkbox" role="switch">` — is void and can hold no thumb. The geometry is not an approximation of shadcn's; it is the same arithmetic, reached from the other side.
| shadcn | here | why |
|---|---|---|
| <SwitchPrimitive.Thumb> | bg-left → checked:bg-right | An `<input>` is void. `border-transparent` makes the positioning area the padding box, so `bg-left` is where `translate-x-0` puts it and `bg-right` is 30px minus a 16px thumb — which is what `calc(100% - 2px)` gives. |
| data-[state=checked]: | checked: / not-checked: | Radix keeps state in a data attribute because a button has none. Written as a bare `bg-input` the off colour would race `dark:` on specificity; `not-checked:` states the condition and there is no race. |
| — | appearance-none | shadcn is styling a button. A checkbox draws its own box, and none of the track has anywhere to sit until that is gone. |
| group/switch | — dropped | It existed so the Thumb could read the Root's size. There is no Thumb to tell, so the two `bg-[size:…]` utilities read `data-size` directly. |
| — (role on the Root) | role="switch" | The one thing the native element does not say for itself: with the role it is announced "on"/"off" rather than "checked"/"unchecked". |
The thumb is a gradient rather than a `::before`, which would have modelled shadcn's translate more directly. A pseudo-element on a replaced element is undefined by the spec — and every other icon in this library is already a background, so this is the consistent answer as well as the safe one. It also keeps the thumb's three colours as tokens: a gradient reads `var(--background)` where a data URI cannot.
The component tag, and its boundary
The tag is the right call for a switch this page drives as much as for one it does not — a `name`, a `value`, a `checked` default, a `required`. What a plain form posts, the tag renders, and every switch above the reactive section is one.
Through the tag
A binding crosses it: a component's root attributes are evaluated where the tag was written, so `checked` and the events that maintain it read and write this page's state.
What the tag does not stamp is a wrapper. A component whose `html!` root is a fragment is delimited by `<!--pp:id-->` comments, which the runtime turns into a real `<pp-fragment>` at hydration — and `peer` would stop reaching across it. Both Switch and Label render one literal element instead, so "Disabled, and a Label that knows" keeps working after mount.
Controlled, page scope
`.render()` returns the element itself, with no boundary around it, so interpolating it into this block puts it in this block's scope and both the binding and the handler resolve. A switch reports itself through `target.checked`, not `target.value` — it is a checkbox under the paint.
{airplane ? "On — the radios are off." : "Off."}