Skip to content
uiify

NativeChoice

Radio or checkbox group: each item is an `<input>`, its `<label>` and an optional panel. 0 kB client JS, at most 0.8 kB gzipped.

Anatomy

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

Live demo

This demo also runs with JavaScript disabled.

NativeChoice.Group

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

PropTypeDefaultDescription
idstring—Required. 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`.
namestring—Documents the `choiceGroupName(id, kind)` override each item must also receive; the group does not read it.

NativeChoice.Item

Renders <div> wrapping <input> + <label> + optional panel <div>. Emits class="uiify-native-choice-item" on the wrapper, class="uiify-native-choice-panel" on the panel.

PropTypeDefaultDescription
valuestring—Required.
labelReactNode—Required. Rendered inside the `<label>`.
panelReactNode—Renders an always-present `<div class="uiify-native-choice-panel">` after the label; reveal it with `:checked ~ .uiify-native-choice-panel`.
groupIdstring—Required. The `NativeChoice.Group.id` this item belongs to.
kind"single" | "multiple"—Required. Must match the group's `kind`.
namestring—Overrides the derived `choiceGroupName(groupId, kind)` form-control name.
defaultCheckedbooleanfalseThe browser owns the state after first render.
disabledbooleanfalseOn the input; `data-disabled` on the wrapper.
slotPropsNativeChoiceItemSlotProps—Native props for the generated input, label, and optional panel.
onValueChange(value: string) => void—Listens to the input's native `change`. Only client callers can pass it; uncontrolled server callers omit it.

Rendered DOM

<div data-orientation="horizontal" id="plan" class="uiify-native-choice"><div class="uiify-native-choice-item"><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 class="uiify-native-choice-item"><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 id="plan-panel-pro" class="uiify-native-choice-panel"><p>Everything in Free, plus...</p></div></div></div>

CSS contract

.uiify-native-choice-panel {
  display: none;
}

input:checked ~ .uiify-native-choice-panel {
  display: block; /* the panel is always in the DOM — reveal it per item */
}

Build your own

import { NativeChoice } from "@gunkin/uiify/elements/native-choice";

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

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

Used by

Gotchas

  • `NativeChoice.Group.id` seeds every child's derived ids and form-control name — it must be unique and stable across SSR, and every `NativeChoice.Item` needs the matching `groupId`.
  • There is no `NativeChoice.Group` → `NativeChoice.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 { NativeChoice } from "@gunkin/uiify/elements/native-choice";