Skip to main content
Nexus Design System
GitHub

ChoiceRow

ChoiceRow makes a whole row the click target for a checkbox or radio choice. The nested Checkbox or RadioGroupItem stays the real control. For a simple inline label use Label, and for a roomier card use ChoiceCard.

'use client';

import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import { ChoiceRow, ChoiceRowTitle } from '@/components/nxds/components/choice-row/choice-row';

export default function ChoiceRowDefault() {
  return (
    <div className="nx:w-80">
      <ChoiceRow htmlFor="choice-default">
        <Checkbox id="choice-default" aria-labelledby="choice-default-title" />
        <ChoiceRowTitle id="choice-default-title">
          Product updates
        </ChoiceRowTitle>
      </ChoiceRow>
    </div>
  );
}

This example also needs: Checkbox.

Installation

npx nxds-cli@latest add choice-row

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import {
  ChoiceRow,
  ChoiceRowContent,
  ChoiceRowDescription,
  ChoiceRowTitle,
} from '@/components/nxds/components/choice-row/choice-row';

Composition

ChoiceRow
├── ChoiceRowTitle
└── ChoiceRowContent
    ├── ChoiceRowTitle
    └── ChoiceRowDescription

Examples

With radio group

Pick one delivery speed from a RadioGroup of rows.

Delivery speed
'use client';

import * as React from 'react';

import { ChoiceRow, ChoiceRowTitle } from '@/components/nxds/components/choice-row/choice-row';
import { FieldLegend, FieldSet } from '@/components/nxds/components/field/field';
import {
  RadioGroup,
  RadioGroupItem,
} from '@/components/nxds/components/radio-group/radio-group';

function NotificationsRadioGroup() {
  const legendId = React.useId();

  return (
    <FieldSet className="nx:w-80">
      <FieldLegend id={legendId}>Delivery speed</FieldLegend>
      <RadioGroup aria-labelledby={legendId}>
        <ChoiceRow htmlFor="choice-radio-standard">
          <RadioGroupItem
            id="choice-radio-standard"
            value="standard"
            aria-labelledby="choice-radio-standard-title"
          />
          <ChoiceRowTitle id="choice-radio-standard-title">
            Standard
          </ChoiceRowTitle>
        </ChoiceRow>
        <ChoiceRow htmlFor="choice-radio-priority">
          <RadioGroupItem
            id="choice-radio-priority"
            value="priority"
            aria-labelledby="choice-radio-priority-title"
          />
          <ChoiceRowTitle id="choice-radio-priority-title">
            Priority
          </ChoiceRowTitle>
        </ChoiceRow>
      </RadioGroup>
    </FieldSet>
  );
}

export default function ChoiceRowWithRadioGroup() {
  return <NotificationsRadioGroup />;
}

This example also needs: Field, RadioGroup.

Disabled

Disable the nested control to lock the choice.

'use client';

import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import { ChoiceRow, ChoiceRowTitle } from '@/components/nxds/components/choice-row/choice-row';

export default function ChoiceRowDisabled() {
  return (
    <div className="nx:w-80">
      <ChoiceRow htmlFor="choice-disabled">
        <Checkbox
          id="choice-disabled"
          disabled
          aria-labelledby="choice-disabled-title"
        />
        <ChoiceRowTitle id="choice-disabled-title">
          Locked notifications
        </ChoiceRowTitle>
      </ChoiceRow>
    </div>
  );
}

With description

Add helper text with ChoiceRowDescription, wired through aria-describedby.

'use client';

import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import {
  ChoiceRow,
  ChoiceRowContent,
  ChoiceRowDescription,
  ChoiceRowTitle,
} from '@/components/nxds/components/choice-row/choice-row';

export default function ChoiceRowWithDescription() {
  return (
    <div className="nx:w-96 nx:max-w-full">
      <ChoiceRow htmlFor="choice-description">
        <Checkbox
          id="choice-description"
          aria-labelledby="choice-description-title"
          aria-describedby="choice-description-help"
        />
        <ChoiceRowContent>
          <ChoiceRowTitle id="choice-description-title">
            Product updates
          </ChoiceRowTitle>
          <ChoiceRowDescription id="choice-description-help">
            News about features and improvements.
          </ChoiceRowDescription>
        </ChoiceRowContent>
      </ChoiceRow>
    </div>
  );
}

Props

ChoiceRow

ChoiceRow adds no props of its own; it accepts the props of the element or primitive it wraps.

ChoiceRowContent

ChoiceRowContent adds no props of its own; it accepts the props of the element or primitive it wraps.

ChoiceRowDescription

ChoiceRowDescription adds no props of its own; it accepts the props of the element or primitive it wraps.

ChoiceRowTitle

ChoiceRowTitle adds no props of its own; it accepts the props of the element or primitive it wraps.

ChoiceRow makes a whole row the click target for a checkbox or radio choice. The nested Checkbox or RadioGroupItem stays the real control. For a simple inline label use Label, and for a roomier card use ChoiceCard.

<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
import { ChoiceRow, ChoiceRowTitle } from '@/components/choice-row';
</script>

<template>
  <div class="nx:w-80">
    <ChoiceRow for="choice-default">
      <Checkbox id="choice-default" aria-labelledby="choice-default-title" />
      <ChoiceRowTitle id="choice-default-title">Product updates</ChoiceRowTitle>
    </ChoiceRow>
  </div>
</template>

This example also needs: Checkbox.

Installation

  1. Install these components first:

    Label.

  2. Copy and paste the following code into your project.

    components/choice-row/ChoiceRow.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import type { LabelProps as RekaLabelProps } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import { Label } from '../label';
    
    export interface ChoiceRowProps extends RekaLabelProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<ChoiceRowProps>();
    
    const delegated = reactiveOmit(props, 'class');
    </script>
    
    <template>
      <Label
        v-bind="delegated"
        data-slot="choice-row"
        :class="
          cn(
            'nx:group/choice-row nx:box-border nx:flex nx:min-h-8 nx:min-w-0 nx:w-full nx:cursor-pointer nx:items-center nx:justify-start nx:gap-2 nx:rounded-sm nx:px-2 nx:py-1.5 nx:transition-colors',
            'nx:not-has-[:disabled]:hover:bg-background-hover nx:not-has-[:disabled]:active:bg-background-active',
            'nx:has-[:disabled]:cursor-not-allowed nx:has-[:disabled]:text-disabled-foreground',
            props.class
          )
        "
      >
        <slot />
      </Label>
    </template>
    
    components/choice-row/ChoiceRowContent.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface ChoiceRowContentProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<ChoiceRowContentProps>();
    </script>
    
    <template>
      <div
        data-slot="choice-row-content"
        :class="
          cn('nx:flex nx:min-w-0 nx:flex-1 nx:flex-col nx:gap-0.5', props.class)
        "
      >
        <slot />
      </div>
    </template>
    
    components/choice-row/ChoiceRowDescription.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface ChoiceRowDescriptionProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<ChoiceRowDescriptionProps>();
    </script>
    
    <template>
      <p
        data-slot="choice-row-description"
        :class="
          cn(
            'nx:m-0 nx:typography-body-default nx:text-muted-foreground nx:group-has-[:disabled]/choice-row:text-disabled-foreground',
            props.class
          )
        "
      >
        <slot />
      </p>
    </template>
    
    components/choice-row/ChoiceRowTitle.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface ChoiceRowTitleProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<ChoiceRowTitleProps>();
    </script>
    
    <template>
      <div
        data-slot="choice-row-title"
        :class="
          cn(
            'nx:min-w-0 nx:flex-1 nx:typography-label-default nx:text-foreground nx:group-has-[:disabled]/choice-row:text-disabled-foreground',
            props.class
          )
        "
      >
        <!-- An unfilled <slot /> still renders fragment anchors, so the title would not be an empty element. -->
        <slot v-if="$slots.default" />
      </div>
    </template>
    
    components/choice-row/index.ts
    export { default as ChoiceRow, type ChoiceRowProps } from './ChoiceRow.vue';
    export {
      default as ChoiceRowContent,
      type ChoiceRowContentProps,
    } from './ChoiceRowContent.vue';
    export {
      default as ChoiceRowDescription,
      type ChoiceRowDescriptionProps,
    } from './ChoiceRowDescription.vue';
    export {
      default as ChoiceRowTitle,
      type ChoiceRowTitleProps,
    } from './ChoiceRowTitle.vue';
    
  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 { ChoiceRow, ChoiceRowContent, ChoiceRowDescription, ChoiceRowTitle } from '@/components/choice-row';

Examples

With radio group

Pick one delivery speed from a RadioGroup of rows.

<script setup lang="ts">
import { useId } from 'vue';

import { ChoiceRow, ChoiceRowTitle } from '@/components/choice-row';
import { FieldLegend, FieldSet } from '@/components/field';
import { RadioGroup, RadioGroupItem } from '@/components/radio-group';

const legendId = useId();
</script>

<template>
  <FieldSet class="nx:w-80">
    <FieldLegend :id="legendId">Delivery speed</FieldLegend>
    <RadioGroup :aria-labelledby="legendId">
      <ChoiceRow for="choice-radio-standard">
        <RadioGroupItem
          id="choice-radio-standard"
          value="standard"
          aria-labelledby="choice-radio-standard-title"
        />
        <ChoiceRowTitle id="choice-radio-standard-title"
          >Standard</ChoiceRowTitle
        >
      </ChoiceRow>
      <ChoiceRow for="choice-radio-priority">
        <RadioGroupItem
          id="choice-radio-priority"
          value="priority"
          aria-labelledby="choice-radio-priority-title"
        />
        <ChoiceRowTitle id="choice-radio-priority-title"
          >Priority</ChoiceRowTitle
        >
      </ChoiceRow>
    </RadioGroup>
  </FieldSet>
</template>

This example also needs: Field, RadioGroup.

Disabled

Disable the nested control to lock the choice.

<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
import { ChoiceRow, ChoiceRowTitle } from '@/components/choice-row';
</script>

<template>
  <div class="nx:w-80">
    <ChoiceRow for="choice-disabled">
      <Checkbox
        id="choice-disabled"
        disabled
        aria-labelledby="choice-disabled-title"
      />
      <ChoiceRowTitle id="choice-disabled-title"
        >Locked notifications</ChoiceRowTitle
      >
    </ChoiceRow>
  </div>
</template>

With description

Add helper text with ChoiceRowDescription, wired through aria-describedby.

<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
import {
  ChoiceRow,
  ChoiceRowContent,
  ChoiceRowDescription,
  ChoiceRowTitle,
} from '@/components/choice-row';
</script>

<template>
  <div class="nx:w-96 nx:max-w-full">
    <ChoiceRow for="choice-description">
      <Checkbox
        id="choice-description"
        aria-labelledby="choice-description-title"
        aria-describedby="choice-description-help"
      />
      <ChoiceRowContent>
        <ChoiceRowTitle id="choice-description-title"
          >Product updates</ChoiceRowTitle
        >
        <ChoiceRowDescription id="choice-description-help"
          >News about features and improvements.</ChoiceRowDescription
        >
      </ChoiceRowContent>
    </ChoiceRow>
  </div>
</template>

Props

ChoiceRow

ChoiceRow adds no props of its own; it accepts the props of the element or primitive it wraps.

ChoiceRowContent

ChoiceRowContent adds no props of its own; it accepts the props of the element or primitive it wraps.

ChoiceRowDescription

ChoiceRowDescription adds no props of its own; it accepts the props of the element or primitive it wraps.

ChoiceRowTitle

ChoiceRowTitle adds no props of its own; it accepts the props of the element or primitive it wraps.