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".
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | 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`. |
name | string | — | 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.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Required. |
label | ReactNode | — | Required. Rendered inside the `<label>`. |
panel | ReactNode | — | Renders an always-present `<div class="uiify-native-choice-panel">` after the label; reveal it with `:checked ~ .uiify-native-choice-panel`. |
groupId | string | — | Required. The `NativeChoice.Group.id` this item belongs to. |
kind | "single" | "multiple" | — | Required. Must match the group's `kind`. |
name | string | — | Overrides the derived `choiceGroupName(groupId, kind)` form-control name. |
defaultChecked | boolean | false | The browser owns the state after first render. |
disabled | boolean | false | On the input; `data-disabled` on the wrapper. |
slotProps | NativeChoiceItemSlotProps | — | 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";