Switch
A control that turns something on, and says so.
src/components/rahti_ui/switch.rs
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
`class` is merged, not appended: a utility written here replaces the one it conflicts with and leaves the rest of the switch alone. The first two rows are the same override written twice, and only one of them works — every size in this component's class list carries a `data-[size=…]:` prefix, and a merge only replaces like with like. The merge is `twMerge`'s, in the browser, so all three of these are a bare checkbox until PulsePoint mounts.
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 does not drive — 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.
Where the tag stops
A binding does not cross it: PulsePoint owns `checked` and the events that maintain it, and takes that ownership in the scope the element is mounted in, which through a tag is the component's.
// Compiles against a scope with no `airplane`, and does nothing.
<Switch checked={airplane} onchange={setAirplane(target.checked)} />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."}