RadioGroup
RadioGroup lets the user pick one option from a short, visible list. For a long list, use Select.
- For a plain text label, pair each RadioGroupItem with Label.
- For a compact clickable row, wrap each item in ChoiceRow.
- For a card with a title and description, wrap each item in ChoiceCard.
'use client';
import { Label } from '@/components/nxds/components/label/label';
import {
RadioGroup,
RadioGroupItem,
} from '@/components/nxds/components/radio-group/radio-group';
export default function RadioGroupDefault() {
return (
<RadioGroup defaultValue="comfortable" aria-label="Density">
<div className="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="default" id="default-default" />
<Label htmlFor="default-default">Default</Label>
</div>
<div className="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="comfortable" id="default-comfortable" />
<Label htmlFor="default-comfortable">Comfortable</Label>
</div>
<div className="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="compact" id="default-compact" />
<Label htmlFor="default-compact">Compact</Label>
</div>
</RadioGroup>
);
}
This example also needs: Label.
Installation
npx nxds-cli@latest add radio-group
Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.
Usage
import {
RadioGroup,
RadioGroupItem,
} from '@/components/nxds/components/radio-group/radio-group';
Composition
RadioGroup
└── RadioGroupItem
Examples
Vertical with labels
Add a short description under each label when options need more context.
For individuals getting started.
For growing teams that need more.
Advanced controls and support.
'use client';
import { Label } from '@/components/nxds/components/label/label';
import {
RadioGroup,
RadioGroupItem,
} from '@/components/nxds/components/radio-group/radio-group';
export default function RadioGroupVerticalWithLabels() {
return (
<RadioGroup defaultValue="starter" aria-label="Plan">
<div className="nx:flex nx:items-start nx:gap-3">
<RadioGroupItem
value="starter"
id="vertical-starter"
className="nx:mt-0.5"
/>
<div className="nx:grid nx:gap-1">
<Label htmlFor="vertical-starter">Starter</Label>
<p className="nx:typography-body-default nx:text-muted-foreground">
For individuals getting started.
</p>
</div>
</div>
<div className="nx:flex nx:items-start nx:gap-3">
<RadioGroupItem value="pro" id="vertical-pro" className="nx:mt-0.5" />
<div className="nx:grid nx:gap-1">
<Label htmlFor="vertical-pro">Pro</Label>
<p className="nx:typography-body-default nx:text-muted-foreground">
For growing teams that need more.
</p>
</div>
</div>
<div className="nx:flex nx:items-start nx:gap-3">
<RadioGroupItem
value="enterprise"
id="vertical-enterprise"
className="nx:mt-0.5"
/>
<div className="nx:grid nx:gap-1">
<Label htmlFor="vertical-enterprise">Enterprise</Label>
<p className="nx:typography-body-default nx:text-muted-foreground">
Advanced controls and support.
</p>
</div>
</div>
</RadioGroup>
);
}
Horizontal
Set orientation="horizontal" and a flex layout to lay options out in a row.
'use client';
import { Label } from '@/components/nxds/components/label/label';
import {
RadioGroup,
RadioGroupItem,
} from '@/components/nxds/components/radio-group/radio-group';
export default function RadioGroupHorizontal() {
return (
<RadioGroup
defaultValue="md"
orientation="horizontal"
className="nx:flex nx:gap-4"
aria-label="Size"
>
<div className="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="sm" id="horizontal-sm" />
<Label htmlFor="horizontal-sm">Small</Label>
</div>
<div className="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="md" id="horizontal-md" />
<Label htmlFor="horizontal-md">Medium</Label>
</div>
<div className="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="lg" id="horizontal-lg" />
<Label htmlFor="horizontal-lg">Large</Label>
</div>
</RadioGroup>
);
}
Disabled
Disable the group so no option can be selected.
'use client';
import { Label } from '@/components/nxds/components/label/label';
import {
RadioGroup,
RadioGroupItem,
} from '@/components/nxds/components/radio-group/radio-group';
export default function RadioGroupDisabled() {
return (
<RadioGroup defaultValue="one" disabled aria-label="Disabled options">
<div className="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="one" id="disabled-one" />
<Label htmlFor="disabled-one">Option one</Label>
</div>
<div className="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="two" id="disabled-two" />
<Label htmlFor="disabled-two">Option two</Label>
</div>
</RadioGroup>
);
}
Invalid
Set aria-invalid on each item to show the error border and focus ring.
'use client';
import { Label } from '@/components/nxds/components/label/label';
import {
RadioGroup,
RadioGroupItem,
} from '@/components/nxds/components/radio-group/radio-group';
export default function RadioGroupInvalid() {
return (
<RadioGroup defaultValue="card" aria-label="Payment method">
<div className="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="card" id="invalid-card" aria-invalid />
<Label htmlFor="invalid-card">Credit card</Label>
</div>
<div className="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="paypal" id="invalid-paypal" aria-invalid />
<Label htmlFor="invalid-paypal">PayPal</Label>
</div>
</RadioGroup>
);
}
Props
RadioGroup
RadioGroup adds no props of its own; it accepts the props of the element or primitive it wraps.
RadioGroupItem
RadioGroupItem adds no props of its own; it accepts the props of the element or primitive it wraps.
RadioGroup lets the user pick one option from a short, visible list. For a long list, use Select.
- For a plain text label, pair each RadioGroupItem with Label.
- For a compact clickable row, wrap each item in ChoiceRow.
- For a card with a title and description, wrap each item in ChoiceCard.
<script setup lang="ts">
import { Label } from '@/components/label';
import { RadioGroup, RadioGroupItem } from '@/components/radio-group';
</script>
<template>
<RadioGroup default-value="comfortable" aria-label="Density">
<div class="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="default" id="default-default" />
<Label for="default-default">Default</Label>
</div>
<div class="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="comfortable" id="default-comfortable" />
<Label for="default-comfortable">Comfortable</Label>
</div>
<div class="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="compact" id="default-compact" />
<Label for="default-compact">Compact</Label>
</div>
</RadioGroup>
</template>
This example also needs: Label.
Installation
- 1
Install the following dependencies:
npm install @phosphor-icons/vue@^2.2.1 @vueuse/core@^14.1.0 reka-ui@^2.11.0 - 2
Copy and paste the following code into your project.
components/radio-group/RadioGroup.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { RadioGroupRoot, type RadioGroupRootEmits, type RadioGroupRootProps, useForwardPropsEmits, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface RadioGroupProps extends RadioGroupRootProps { class?: HTMLAttributes['class']; } const props = defineProps<RadioGroupProps>(); const emits = defineEmits<RadioGroupRootEmits>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardPropsEmits(delegated, emits); </script> <template> <RadioGroupRoot v-bind="forwarded" data-slot="radio-group" :class="cn('nx:grid nx:gap-2', props.class)" > <slot /> </RadioGroupRoot> </template>components/radio-group/RadioGroupItem.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { RadioGroupIndicator, RadioGroupItem, type RadioGroupItemProps as RekaRadioGroupItemProps, useForwardProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { IconCircleFilled } from '../../lib/icons'; import { selectionIndicatorMotionClassName } from '../../lib/motion'; import { cn } from '../../lib/utils'; export interface RadioGroupItemProps extends RekaRadioGroupItemProps { class?: HTMLAttributes['class']; } const props = defineProps<RadioGroupItemProps>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardProps(delegated); </script> <template> <RadioGroupItem v-bind="forwarded" data-slot="radio-group-item" :class=" cn( 'nx:group nx:size-4 nx:shrink-0 nx:cursor-pointer nx:rounded-full nx:p-0 nx:border-default nx:border-border-default nx:bg-container', 'nx:transition-control', 'nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default', 'nx:aria-invalid:border-error-border nx:aria-invalid:focus-visible:outline-focus-error', 'nx:disabled:cursor-not-allowed nx:disabled:border-border-disabled nx:disabled:bg-disabled', 'nx:data-[state=checked]:border-primary-background nx:data-[state=checked]:text-primary-background nx:data-[state=checked]:disabled:border-primary-disabled', 'nx:aria-invalid:data-[state=checked]:border-error-border nx:aria-invalid:data-[state=checked]:text-error-background', props.class ) " > <RadioGroupIndicator force-mount data-slot="radio-group-indicator" class="nx:flex nx:items-center nx:justify-center" > <IconCircleFilled data-slot="radio-group-dot" aria-hidden="true" :class=" cn( 'nx:size-2.5 nx:text-current', selectionIndicatorMotionClassName, 'nx:group-data-[state=checked]:scale-100 nx:group-data-[state=checked]:opacity-100' ) " /> </RadioGroupIndicator> </RadioGroupItem> </template>components/radio-group/index.ts export { default as RadioGroup, type RadioGroupProps } from './RadioGroup.vue'; export { default as RadioGroupItem, type RadioGroupItemProps, } from './RadioGroupItem.vue';lib/icons.ts /** * Internal Icons * * This barrel file re-exports icons used internally by the design system. * These are hardcoded icons for component states (loading, validation, navigation). * * User-facing icons should be passed via props by consumers. * * This set draws Phosphor. `nxds init --icons` installs the same names drawn * by another library. */ import { PhCircle } from '@phosphor-icons/vue'; import { type FunctionalComponent, h } from 'vue'; // Loading states export { PhCircleNotch as IconLoader2 } from '@phosphor-icons/vue'; // Navigation export { PhArrowDown as IconArrowDown, PhArrowLeft as IconArrowLeft, PhArrowRight as IconArrowRight, PhArrowUp as IconArrowUp, PhCaretDown as IconChevronDown, PhCaretLeft as IconChevronLeft, PhCaretRight as IconChevronRight, PhCaretUp as IconChevronUp, PhDotsThree as IconDots, PhSidebarSimple as IconLayoutSidebar, PhCaretUpDown as IconSelector, } from '@phosphor-icons/vue'; // Actions export { PhCheck as IconCheck, PhCopy as IconCopy, PhMinus as IconMinus, PhPencilSimple as IconPencil, PhPlus as IconPlus, PhX as IconX, } from '@phosphor-icons/vue'; export const IconCircleFilled: FunctionalComponent = (_props, { attrs }) => h(PhCircle, { weight: 'fill', ...attrs }); // Filter field types export { PhCalendarBlank as IconCalendar, PhHash as IconHash, PhTextAa as IconLetterCase, PhListBullets as IconList, } from '@phosphor-icons/vue'; // Search / input export { PhMagnifyingGlass as IconSearch } from '@phosphor-icons/vue'; // Alerts/Status export { PhWarningCircle as IconAlertCircle, PhWarning as IconAlertTriangle, PhCheckCircle as IconCircleCheck, PhInfo as IconInfoCircle, } from '@phosphor-icons/vue'; // Layout export { PhDotsSixVertical as IconGripVertical, PhPalette as IconPalette, } from '@phosphor-icons/vue';lib/motion.ts export const selectionIndicatorMotionClassName = 'nx:scale-50 nx:opacity-0 nx:transition-[opacity,scale] nx:duration-fast nx:ease-enter'; - 3
Keep each file at the path shown above it.
Paths are relative to
packages/vue/src/. Keep the same folders under your@/root, and the imports between files work without edits.
Usage
import { RadioGroup, RadioGroupItem } from '@/components/radio-group';
Examples
Vertical with labels
Add a short description under each label when options need more context.
<script setup lang="ts">
import { Label } from '@/components/label';
import { RadioGroup, RadioGroupItem } from '@/components/radio-group';
</script>
<template>
<RadioGroup default-value="starter" aria-label="Plan">
<div class="nx:flex nx:items-start nx:gap-3">
<RadioGroupItem value="starter" id="vertical-starter" class="nx:mt-0.5" />
<div class="nx:grid nx:gap-1">
<Label for="vertical-starter">Starter</Label>
<p class="nx:typography-body-default nx:text-muted-foreground">
For individuals getting started.
</p>
</div>
</div>
<div class="nx:flex nx:items-start nx:gap-3">
<RadioGroupItem value="pro" id="vertical-pro" class="nx:mt-0.5" />
<div class="nx:grid nx:gap-1">
<Label for="vertical-pro">Pro</Label>
<p class="nx:typography-body-default nx:text-muted-foreground">
For growing teams that need more.
</p>
</div>
</div>
<div class="nx:flex nx:items-start nx:gap-3">
<RadioGroupItem
value="enterprise"
id="vertical-enterprise"
class="nx:mt-0.5"
/>
<div class="nx:grid nx:gap-1">
<Label for="vertical-enterprise">Enterprise</Label>
<p class="nx:typography-body-default nx:text-muted-foreground">
Advanced controls and support.
</p>
</div>
</div>
</RadioGroup>
</template>
Horizontal
Set orientation="horizontal" and a flex layout to lay options out in a row.
<script setup lang="ts">
import { Label } from '@/components/label';
import { RadioGroup, RadioGroupItem } from '@/components/radio-group';
</script>
<template>
<RadioGroup
default-value="md"
orientation="horizontal"
class="nx:flex nx:gap-4"
aria-label="Size"
>
<div class="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="sm" id="horizontal-sm" />
<Label for="horizontal-sm">Small</Label>
</div>
<div class="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="md" id="horizontal-md" />
<Label for="horizontal-md">Medium</Label>
</div>
<div class="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="lg" id="horizontal-lg" />
<Label for="horizontal-lg">Large</Label>
</div>
</RadioGroup>
</template>
Disabled
Disable the group so no option can be selected.
<script setup lang="ts">
import { Label } from '@/components/label';
import { RadioGroup, RadioGroupItem } from '@/components/radio-group';
</script>
<template>
<RadioGroup default-value="one" disabled aria-label="Disabled options">
<div class="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="one" id="disabled-one" />
<Label for="disabled-one">Option one</Label>
</div>
<div class="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="two" id="disabled-two" />
<Label for="disabled-two">Option two</Label>
</div>
</RadioGroup>
</template>
Invalid
Set aria-invalid on each item to show the error border and focus ring.
<script setup lang="ts">
import { Label } from '@/components/label';
import { RadioGroup, RadioGroupItem } from '@/components/radio-group';
</script>
<template>
<RadioGroup default-value="card" aria-label="Payment method">
<div class="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="card" id="invalid-card" aria-invalid="true" />
<Label for="invalid-card">Credit card</Label>
</div>
<div class="nx:flex nx:items-center nx:gap-2">
<RadioGroupItem value="paypal" id="invalid-paypal" aria-invalid="true" />
<Label for="invalid-paypal">PayPal</Label>
</div>
</RadioGroup>
</template>
Props
RadioGroup
RadioGroup adds no props of its own; it accepts the props of the element or primitive it wraps.
RadioGroupItem
RadioGroupItem adds no props of its own; it accepts the props of the element or primitive it wraps.