Radio Group
A set of options where exactly one is chosen.
src/components/rahti_ui/radio_group.rs
Basics
Three options, one `name`, all rendered by the server. Click one and the others let go; press an arrow key and the selection moves; press Tab and the group is one stop, not three. None of that is this component — it is what the browser does with a shared `name`, and it is the whole reason these are `<input type="radio">` rather than shadcn's `<button role="radio">`.
States
A `false` leaves the attribute off rather than writing `checked="false"`, which HTML would read as chosen. `.invalid(true)` writes `aria-invalid`, and the `aria-invalid:` utilities in the class list do the rest — the styling and the accessible state are the same fact, spelled once.
`disabled` is per option: a group is handed its children already rendered and cannot reach into them.
Choice cards
The same two components, with the Label doing the work. `Label`'s class list is `flex items-center gap-2`, so a card only has to override `items-center` to line a two-line description up with the dial — and `has-[:checked]:` paints the chosen card without anyone tracking which one it is.
Disabled, and a Label that knows
This is why `peer` was added to shadcn's class list. 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 RadioGroupItem and a Label with nothing overridden, each rendering as one literal element, so the dial really is the label's preceding sibling.
A fieldset, and the name the group needs
`role="radiogroup"` with no accessible name is a set of options nobody is told the purpose of. A `<fieldset>` with a `<legend>` is HTML's own answer and needs no ARIA at all; `aria-labelledby` on the RadioGroup tag is the other. The class list is reached through `radio_group_variants` here, because the element has to be a fieldset and the tag renders a div.
As a form posts it
One `name`, two `value`s, and `?shipping=express` in the query string — HTML's own way to post a choice, with no JavaScript and no state in the page. Submit without choosing and the browser refuses: `required` on any one option makes the group required, which is HTML's rule rather than this component's to restate.
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 group here carries an `aria-label` instead of a heading to point at.
Class override
`class` is merged, not appended: a utility written here replaces the one it conflicts with and leaves the rest of the dial alone. The merge is `twMerge`'s, in the browser, so unlike every other dial on this page these three are unstyled until PulsePoint mounts — and on a radio that shows, because `appearance-none` is one of the utilities that is missing until then. The group's own `gap-1` is overridden the same way.
What this port changed
shadcn's RadioGroup is a Radix Root that owns `value`, `onValueChange` and a roving `tabindex`, over Items that are `<button role="radio">` with an Indicator child and a hidden input beside each for the form. Every part of that is JavaScript reimplementing what a shared `name` already does — one selection, arrow keys, one tab stop, a value in the post — so this port keeps the attribute and drops the primitive.
| shadcn | here | why |
|---|---|---|
| <RadioGroup value onValueChange> | name="…" on each item | The Root is a `name`, in JavaScript. The group here is a `<div role="radiogroup">` with `grid gap-3` on it and no state of its own. |
| <CircleIcon /> | radial-gradient | An `<input>` is void and can hold no children. A gradient is the one background image that can read `var(--primary)`, so the dot is one utility rather than the Checkbox's baked hex per theme. |
| — | appearance-none | shadcn is styling a button. A radio draws its own dial, and shadcn's border and background have nothing to sit on until that is gone. |
| — (Base UI: peer) | peer | One class, so a Label written beside the dial can read its disabled state. shadcn's own Base UI radio carries it for exactly this. |
Everything else — every colour, every ring, every `dark:` — is shadcn's, copied rather than reinterpreted.
Controlled, page scope
`change` bubbles, so one handler on the group is the whole of Radix's `onValueChange` — a radio reports itself through `target.value`, and only the option being turned *on* fires the event. The elements are written with `radio_group_variants` and `radio_group_item_variants` rather than as tags, because a binding is compiled in the block its element sits in and these have to sit in this one.
Chosen: {plan}
The component tags, and their boundary
The tags are the right call for a group this page does not drive — a `name`, a `value`, a `checked` default, a `required`. What a plain form posts, the tags render, and every option 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 `plan`, and does nothing.
<RadioGroupItem checked={plan === 'pro'} onchange={setPlan(target.value)} />That sample is also why this section is a helper rather than part of the page's own block: a `{…}` in a text node is what the runtime compiles, so quoting a binding inside a mounted block runs it. A block with no script is never mounted, and never reads the text.
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 RadioGroupItem and Label render one literal element instead, so "Disabled, and a Label that knows" keeps working after mount.