⟦Škíp tó çóñtéñt~~~~~~⟧
uiify

Choice

⟦Rádíó ór çhéçkbóx gróúp: éáçh ítém íš áñ `<íñpút>`, ítš `<lábél>` áñd áñ óptíóñál páñél.~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~⟧

0 kB client JS≤ 0.8 kB gzipped

Anatomy

<Choice.Group id="plan" kind="single" orientation="horizontal">
  <Choice.Item defaultChecked groupId="plan" kind="single" label="Free" value="free" />
  <Choice.Item
    groupId="plan"
    kind="single"
    label="Pro"
    panel={<p>Everything in Free, plus...</p>}
    value="pro"
  />
</Choice.Group>

Live demo

This demo also runs with JavaScript disabled.

Choice

Minimal — single

Composition — multiple with panels

Adds 50 GB of storage to your plan.

Not available on your current plan.

Choice.Group

Renders <div>. Emits data-orientation, data-uiify-choice="".

NameTypeDefaultNotes
idstringRequired. Seeds every child item's derived ids and form-control name; must be stable across SSR.
kind"single" | "multiple"`single` renders `type="radio"` items, `multiple` renders `type="checkbox"`.
orientation"horizontal" | "vertical""horizontal"Emits `data-orientation`.
namestringDocuments the `choiceGroupName(id, kind)` override each item must also receive; the group does not read it.

Choice.Item

Renders <div> wrapping <input> + <label> + optional panel <div>. Emits data-part (default "item"), data-uiify-choice="".

NameTypeDefaultNotes
valuestringRequired.
labelReactNodeRequired. Rendered inside the `<label>`.
panelReactNodeRenders an always-present `<div data-part="panel">` after the label; reveal it with `:checked ~ [data-part=panel]`.
groupIdstringRequired. The `Choice.Group.id` this item belongs to.
kind"single" | "multiple"Required. Must match the group's `kind`.
namestringOverrides the derived `choiceGroupName(groupId, kind)` form-control name.
defaultCheckedbooleanfalseThe browser owns the state after first render.
disabledbooleanfalseOn the input; `data-disabled` on the wrapper.
partstring"item"Wrapper `data-part` (custom choice consumers use `"step"`).
labelPartbooleanfalseAdds `data-part="label"` to the label.
inputProps / labelProps / panelPropsRecord<string, unknown>Escape hatch: extra attributes spread onto the input/label/panel.
onValueChange(value: string) => voidListens to the input's native `change`. Only client callers can pass it; uncontrolled server callers omit it.

Rendered DOM

<div id="plan" data-orientation="horizontal" data-uiify-choice=""><div data-part="item" data-uiify-choice=""><input aria-labelledby="plan-label-free" id="plan-control-free" type="radio" name="plan" checked="" value="free"/><label for="plan-control-free" id="plan-label-free">Free</label></div><div data-part="item" data-uiify-choice=""><input aria-labelledby="plan-label-pro" id="plan-control-pro" type="radio" name="plan" value="pro"/><label for="plan-control-pro" id="plan-label-pro">Pro</label><div data-part="panel" id="plan-panel-pro"><p>Everything in Free, plus...</p></div></div></div>

CSS contract

[data-part=panel] {
  display: none;
}

input:checked ~ [data-part=panel] {
  display: block; /* the panel is always in the DOM — reveal it per item */
}

Build your own

import { Choice } from "@gunkin/uiify/elements/choice";

const filters = ["Open", "Closed", "Archived"];

export function FilterChips() {
  return (
    <Choice.Group id="status-filter" kind="multiple" orientation="horizontal">
      {filters.map((filter) => (
        <Choice.Item
          groupId="status-filter"
          key={filter}
          kind="multiple"
          label={filter}
          value={filter.toLowerCase()}
        />
      ))}
    </Choice.Group>
  );
}

Used by

Gotchas

  • `Choice.Group.id` seeds every child's derived ids and form-control name — it must be unique and stable across SSR, and every `Choice.Item` needs the matching `groupId`.
  • There is no `Choice.Group` → `Choice.Item` context — `groupId` and `kind` are required on every item, not inherited.
  • The panel is always in the DOM when given — revealing it only when checked is a CSS rule you write, not something the element does for you.

Import

import { Choice } from "@gunkin/uiify/elements/choice";