Toggle
A button that stays pressed.
src/components/rahti_ui/toggle.rs
Basics
Click one and it stays pressed; click it again and it lets go. Nothing on this page is holding that state — it is a checkbox inside the label, so the browser is. Tab reaches each one and the space bar toggles it, and all of that is true with JavaScript switched off, which shadcn's `<button aria-pressed>` and its `useState` cannot say.
Variants
shadcn's two. The interesting half is the hover: shadcn puts `hover:bg-muted` in the base and lets `outline`'s `hover:bg-accent` overrule it through `twMerge`, and this library joins its layers instead of merging them — so each variant carries its own hover and neither has to beat the other. Same rendered class list, and the layer audit can prove it rather than trust it.
Sizes
shadcn's three, and each is `min-w-*` rather than `w-*` — a toggle holding one icon comes out square, and one holding a word grows to fit it. The icons are sized by `[&_svg:not([class*='size-'])]:size-4`, which is shadcn's and which works here because the element carrying it is a label with children.
With text, and with both
What sits between the tags is what the toggle shows, and `inline-flex items-center gap-2` lays it out. This is the thing the Checkbox port could not do: an `<input>` is void, so its tick had to be a background image and shadcn's icon utilities were dropped. Here the visible element is a `<label>`, so the children are real children and all three come across untouched.
States
Every one of these is a `:has()` question the label asks its input — `has-[:disabled]:opacity-50` where shadcn writes `disabled:opacity-50`, and `has-[[aria-invalid=true]]:border-destructive` where it writes `aria-invalid:border-destructive`. The declarations are shadcn's; only the question changed, because the state is on the control and the paint is on the label. `aria-invalid` shows on the outline variant, since the default one has no border to colour.
As a form posts it
A toggle posts, which a `<button aria-pressed>` does not. Each has its own `name`, and a checkbox posts nothing at all when it is off — so `?bold=on` means pressed and an absent key means not, which is HTML's rule rather than this component's to restate. No hidden field beside the control to carry the state, because the control *is* the state.
Attributes, and which element they reach
A toggle is two elements, so the builder says which one it means. `.attr(…)` and `.attrs(…)` reach the `<label>`, where `class` and `data-slot` live; `.input_attr(…)` and `.input_attrs(…)` reach the `<input>`, and so do `name`, `value`, `pressed`, `disabled`, `required`, `invalid`, every binding and every handler — because that is where a form reads it and where `change` fires. The middle toggle has no visible text, so its `aria-label` goes on the control rather than on the paint.
Class override
`class` is merged, not appended: a utility written here replaces the one it conflicts with and leaves the rest alone. The middle one is the case worth reading — the pressed colours are `has-[:checked]:` utilities, so an override that means to recolour the pressed state has to carry the same prefix. A plain `bg-emerald-600` would sit beside them and lose. The merge is `twMerge`'s, in the browser, so all three are unstyled until PulsePoint mounts.
What this port changed
shadcn's Toggle is a Radix Root: a `<button aria-pressed>` carrying `data-state="on"`, with a `useState` above it, because a button does not stay pressed on its own. This is a `<label>` wrapping a hidden `<input type="checkbox">`, which does — and it is the first component in this library that is two elements rather than one.
| shadcn | here | why |
|---|---|---|
| <button aria-pressed> | <label> + hidden <input> | The state has to live in a checkbox, and a checkbox is void — it can hold no icon. So the checkbox holds the state and the label holds the appearance, which is the arrangement HTML has for exactly this. |
| data-[state=on]: | has-[:checked]: | The label has no state either — but its input does, and `:has()` is how one element asks about another. Every state utility moves the same way. |
| [&_svg]:size-4 | [&_svg]:size-4 | Unchanged — and worth a row, because the Checkbox port had to drop these. A label has children where an input has none. |
| — (base hover, overruled) | hover on each variant | shadcn's base and its `outline` variant both set a hover background and let `twMerge` decide. This library joins its layers, so the hover moved down and the audit can prove there is nothing left to resolve. |
| toggle button, pressed | checkbox, checked | The cost, stated plainly. A screen reader announces this as a checkbox rather than as a pressed button. `role="switch"` was available to the Switch because a switch is a two-state control; there is no role that makes a checkbox a toggle button without taking its behaviour away with it. |
Everything else — every colour, every ring, both variants, all three sizes — is shadcn's, copied rather than reinterpreted.
The component tag, and its boundary
The tag is the right call for a toggle this page does not drive — a `name`, a `value`, a `pressed` default. What a plain form posts, the tag renders, and every toggle 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 `bold`, and does nothing.
<Toggle checked={bold} onchange={setBold(target.checked)}>"Bold"</Toggle>Children handed to a tag are compiled where they were written too, so a toggle whose *label* is the page's state wants `toggle_variants` on a native `<label>`. What this component does not have to worry about is `peer`: its label is its own wrapper, and nothing outside reaches across it with a combinator — which is what made the Checkbox and the Switch render as one literal element rather than a fragment.
Controlled, page scope
`.render(…)` returns the two elements themselves, with no boundary around them, so interpolating into this block puts both in this block's scope and the binding and the handler resolve. A toggle reports itself through `target.checked` — under the paint it is a checkbox, and that is the whole point of the port.
The quick brown fox.