Files
zeekayandHanzo Dev 751ef5c778 rename the framework to @hanzo/gui and the loader to @hanzogui/loader
Both old names are owned by a different npm account (artemis-prime), so
publishing them returns 403 — which is how this surfaced: the 8.0.0 run
shipped @hanzogui/logo and @hanzogui/demos depending on [email protected], a
version that cannot be published and therefore does not exist. Those two
were uninstallable.

@hanzo/gui and @hanzogui/* are ours, so the framework now carries the name
it is imported by everywhere downstream, and the loader moves under the
scope we control.

The rewrite is limited to package references. `DEBUG?.startsWith('hanzogui')`
is a log-namespace filter and join(dir, 'hanzogui') is a path — rewriting
either would change behaviour, so both are untouched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-30 20:23:18 -07:00

451 lines
11 KiB
Plaintext

---
title: RadioGroup
description: Use in a form to allow selecting one option from multiple
name: radio-group
component: RadioGroup
package: radio-group
demoName: RadioGroup
---
<YStack className="is-sticky" />
<Tabs id="type" defaultValue="styled">
<Tabs.List>
<TooltipSimple label="With Gui's default styles">
<Tabs.Tab value="styled">Styled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No default styles, easy to customize">
<Tabs.Tab value="unstyled">Unstyled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No dependency on Gui's core">
<Tabs.Tab value="headless" label="No styles and no dependency on Gui's styling">Headless</Tabs.Tab>
</TooltipSimple>
</Tabs.List>
<HeroContainer>
<Tabs.Content value="styled" justifyContent="center" alignItems="center" width="100%">
<RadioGroupDemo />
</Tabs.Content>
<Tabs.Content value="unstyled" justifyContent="center" alignItems="center" width="100%">
<RadioGroupDemo />
</Tabs.Content>
<Tabs.Content value="headless" justifyContent="center" alignItems="center" width="100%">
<RadioGroupHeadlessDemo />
</Tabs.Content>
</HeroContainer>
<Tabs.Content value="styled">
```tsx hero template=RadioGroup
````
</Tabs.Content>
<Tabs.Content value="unstyled">
```tsx hero template=RadioGroup
````
</Tabs.Content>
<Tabs.Content value="headless">
```tsx hero template=RadioGroupHeadless
````
</Tabs.Content>
<Highlights
features={[
`Accessible, easy to compose and customize.`,
`Sizable & works controlled or uncontrolled.`,
`Ability to opt-out to native radio button on web.`,
]}
/>
## Installation
<Tabs.Content value="styled">
RadioGroup is already installed in `hanzogui`, or you can install it independently:
```bash
npm install @hanzogui/radio-group
````
</Tabs.Content>
<Tabs.Content value="unstyled">
RadioGroup is already installed in `hanzogui`, or you can install it independently:
```bash
npm install @hanzogui/radio-group
```
</Tabs.Content>
<Tabs.Content value="headless">
To use the headless radio group, import from the `@hanzogui/radio-headless` package. This package has no dependency on `@hanzogui/core` and provides hooks for building custom radio groups with any styling solution.
```bash
npm install @hanzogui/radio-headless
```
</Tabs.Content>
## Usage
<Tabs.Content value="styled">
```tsx
import { RadioGroup } from '@hanzo/gui'
export default () => (
<RadioGroup value="foo" gap="$2">
<RadioGroup.Item value="foo" id="foo-radio-item">
<RadioGroup.Indicator />
</RadioGroup.Item>
<RadioGroup.Item value="bar" id="bar-radio-item">
<RadioGroup.Indicator />
</RadioGroup.Item>
</RadioGroup>
)
```
</Tabs.Content>
<Tabs.Content value="unstyled">
Use the `createRadioGroup` export to create a fully custom radio group that still uses the Gui styling system. You provide your own styled components and get back a fully functional radio group component.
```tsx
import { createRadioGroup, RadioGroupContext } from '@hanzogui/radio-group'
import { styled, View } from '@hanzo/gui'
const CustomFrame = styled(View, {
// your styles
})
const CustomItem = styled(View, {
context: RadioGroupContext,
// your styles
})
const CustomIndicator = styled(View, {
context: RadioGroupContext,
// your styles
})
export const CustomRadioGroup = createRadioGroup({
Frame: CustomFrame,
Item: CustomItem,
Indicator: CustomIndicator,
})
```
</Tabs.Content>
<Tabs.Content value="headless">
The `@hanzogui/radio-headless` package provides three hooks for building custom radio groups:
- `useRadioGroup` - for the container/group
- `useRadioGroupItem` - for individual radio items
- `useRadioGroupItemIndicator` - for the indicator (checked state visual)
### Basic Usage
```tsx
import {
useRadioGroup,
useRadioGroupItem,
useRadioGroupItemIndicator,
RadioGroupContextValue,
RadioGroupItemContextValue,
} from '@hanzogui/radio-headless'
import { createContext, useContext } from 'react'
// Create contexts for the group and item
const RadioGroupContext = createContext<RadioGroupContextValue>({})
const RadioGroupItemContext = createContext<RadioGroupItemContextValue>({
checked: false,
})
function RadioGroup({ children, ...props }) {
const { providerValue, frameAttrs } = useRadioGroup({
orientation: 'vertical',
...props,
})
return (
<RadioGroupContext.Provider value={providerValue}>
<div {...frameAttrs} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{children}
</div>
</RadioGroupContext.Provider>
)
}
function RadioItem({ value, children }) {
const { checked, providerValue, frameAttrs, bubbleInput } = useRadioGroupItem({
radioGroupContext: RadioGroupContext,
value,
})
return (
<RadioGroupItemContext.Provider value={providerValue}>
<button
{...frameAttrs}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: 8,
border: '1px solid #ccc',
borderRadius: 4,
background: checked ? '#e0f2fe' : 'white',
}}
>
<RadioIndicator />
{children}
</button>
{bubbleInput}
</RadioGroupItemContext.Provider>
)
}
function RadioIndicator() {
const { checked, ...indicatorProps } = useRadioGroupItemIndicator({
radioGroupItemContext: RadioGroupItemContext,
})
return (
<div
{...indicatorProps}
style={{
width: 16,
height: 16,
borderRadius: '50%',
border: '2px solid #3b82f6',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{checked && (
<div
style={{ width: 8, height: 8, borderRadius: '50%', background: '#3b82f6' }}
/>
)}
</div>
)
}
// Usage
function App() {
return (
<RadioGroup defaultValue="option1" onValueChange={console.log}>
<RadioItem value="option1">Option 1</RadioItem>
<RadioItem value="option2">Option 2</RadioItem>
<RadioItem value="option3">Option 3</RadioItem>
</RadioGroup>
)
}
```
</Tabs.Content>
## API Reference
### RadioGroup
<Tabs.Content value="styled">
RadioGroup extends Stack views inheriting all the [Gui standard props](/docs/intro/props), plus:
</Tabs.Content>
<Tabs.Content value="unstyled">
When using `createRadioGroup`, you provide styled components:
- `Frame`: The root container component
- `Item`: The radio item component
- `Indicator`: The indicator component
</Tabs.Content>
<Tabs.Content value="headless">
The `useRadioGroup` hook accepts these options:
</Tabs.Content>
<PropsTable
data={[
{
name: 'name',
type: 'string',
description: `Equivalent to input name attribute.`,
},
{
name: 'value',
type: 'string',
description: `Controlled value of the selected radio item.`,
},
{
name: 'defaultValue',
type: 'string',
description: `Default value for uncontrolled usage.`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required attribute.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Sets aria-disabled on web, and disables touch on native for all children items.`,
},
{
name: 'native',
type: 'boolean',
description: `Renders native radio button on web.`,
default: 'false',
},
{
name: 'onValueChange',
type: '(value: string) => void',
description: 'Called when the selected value changes.',
},
{
name: 'orientation',
type: '"horizontal" | "vertical"',
description: 'Orientation of the radio group.',
},
{
name: 'accentColor',
type: 'string',
description: 'Sets `accent-color` style when `native` prop is enabled.',
},
]}
/>
<Tabs.Content value="styled">
### RadioGroup.Item
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Sets aria-labelledby on web.`,
},
{
name: 'value',
type: 'string',
description: `Input value for the radio button.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Sets aria-disabled on web, and disables touch on native.`,
},
{
name: 'id',
type: 'string',
description: `ID used on the web.`,
},
{
name: 'scaleSize',
type: 'number',
default: '0.5',
description: `Scale factor for the radio item size. Gui size tokens map to button heights, so you typically want smaller radio buttons.`,
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, removes all default Gui styling.`,
},
]}
/>
### RadioGroup.Indicator
RadioGroup.Indicator appears only when the parent Item is checked. It extends [ThemeableStack](/docs/components/stacks#themeablestack), getting [Gui standard props](/docs/intro/props) adding:
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Gui styles.`,
},
]}
/>
</Tabs.Content>
<Tabs.Content value="headless">
### useRadioGroup Return Value
| Property | Type | Description |
| ----------------------- | ------------------------ | --------------------------------------------------------------------- |
| `providerValue` | `RadioGroupContextValue` | Value to pass to your context provider |
| `frameAttrs` | `object` | Props to spread on the group container (role, aria-orientation, etc.) |
| `rovingFocusGroupAttrs` | `object` | Props for roving focus behavior (optional) |
### useRadioGroupItem
Hook for individual radio items. Requires a context containing the group state.
```tsx
const {
checked,
isFormControl,
providerValue,
bubbleInput,
native,
frameAttrs,
rovingFocusGroupAttrs,
} = useRadioGroupItem({
radioGroupContext: YourRadioGroupContext,
value: 'option1',
id: 'option1-id',
labelledBy: 'label-id',
disabled: false,
onPress: () => {},
onKeyDown: () => {},
onFocus: () => {},
})
```
| Property | Type | Description |
| --------------- | ----------- | --------------------------------------- |
| `checked` | `boolean` | Whether this item is currently selected |
| `providerValue` | `object` | Context value for the indicator |
| `frameAttrs` | `object` | Props to spread on the item element |
| `bubbleInput` | `ReactNode` | Hidden input for form compatibility |
| `native` | `boolean` | Whether using native radio |
| `isFormControl` | `boolean` | Whether inside a form |
### useRadioGroupItemIndicator
Hook for the visual indicator of checked state.
```tsx
const { checked, ...dataAttrs } = useRadioGroupItemIndicator({
radioGroupItemContext: YourRadioGroupItemContext,
disabled: false,
})
```
| Property | Type | Description |
| --------------- | --------------------- | ---------------------------------- |
| `checked` | `boolean` | Whether the parent item is checked |
| `data-state` | `string` | 'checked' or 'unchecked' |
| `data-disabled` | `string \| undefined` | Present when disabled |
</Tabs.Content>
</Tabs>