Skip to main content
Nexus Design System
GitHub

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.