ChoiceCard
ChoiceCard is a clickable card for a checkbox or radio choice with a title and description. The nested Checkbox or RadioGroupItem stays the only control. For a simple inline label use Label, and for compact options use ChoiceRow.
'use client';
import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
ChoiceCardTitle,
} from '@/components/nxds/components/choice-card/choice-card';
export default function ChoiceCardDefault() {
return (
<div className="nx:w-96 nx:max-w-full">
<ChoiceCard htmlFor="choice-card-default" variant="bordered">
<Checkbox
id="choice-card-default"
aria-labelledby="choice-card-default-title"
aria-describedby="choice-card-default-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-default-title">
Product updates
</ChoiceCardTitle>
<ChoiceCardDescription id="choice-card-default-description">
News about features and improvements.
</ChoiceCardDescription>
</ChoiceCardContent>
</ChoiceCard>
</div>
);
}
This example also needs: Checkbox.
Installation
npx nxds-cli@latest add choice-card
Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.
Usage
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
ChoiceCardTitle,
} from '@/components/nxds/components/choice-card/choice-card';
Composition
ChoiceCard
└── ChoiceCardContent
├── ChoiceCardTitle
└── ChoiceCardDescription
ChoiceCardContent
├── ChoiceCardTitle
└── ChoiceCardDescription
Examples
With radio group
Pick one plan from a RadioGroup of cards.
'use client';
import * as React from 'react';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
type ChoiceCardProps,
ChoiceCardTitle,
} from '@/components/nxds/components/choice-card/choice-card';
import { FieldLegend, FieldSet } from '@/components/nxds/components/field/field';
import {
RadioGroup,
RadioGroupItem,
} from '@/components/nxds/components/radio-group/radio-group';
type ChoiceCardVariant = ChoiceCardProps['variant'];
interface PlanRadioCardsProps {
idPrefix?: string;
variant?: ChoiceCardVariant;
}
function PlanRadioCards({
idPrefix = 'choice-card-radio',
variant,
}: PlanRadioCardsProps = {}) {
const legendId = React.useId();
const standardId = `${idPrefix}-standard`;
const priorityId = `${idPrefix}-priority`;
return (
<FieldSet className="nx:w-96 nx:max-w-full">
<FieldLegend id={legendId}>Plan</FieldLegend>
<RadioGroup aria-labelledby={legendId}>
<ChoiceCard htmlFor={standardId} variant={variant}>
<RadioGroupItem
id={standardId}
value="standard"
aria-labelledby={`${standardId}-title`}
aria-describedby={`${standardId}-description`}
/>
<ChoiceCardContent>
<ChoiceCardTitle id={`${standardId}-title`}>
Standard
</ChoiceCardTitle>
<ChoiceCardDescription id={`${standardId}-description`}>
Best for most teams.
</ChoiceCardDescription>
</ChoiceCardContent>
</ChoiceCard>
<ChoiceCard htmlFor={priorityId} variant={variant}>
<RadioGroupItem
id={priorityId}
value="priority"
aria-labelledby={`${priorityId}-title`}
aria-describedby={`${priorityId}-description`}
/>
<ChoiceCardContent>
<ChoiceCardTitle id={`${priorityId}-title`}>
Priority
</ChoiceCardTitle>
<ChoiceCardDescription id={`${priorityId}-description`}>
Faster response routing for urgent teams.
</ChoiceCardDescription>
</ChoiceCardContent>
</ChoiceCard>
</RadioGroup>
</FieldSet>
);
}
export default function ChoiceCardWithRadioGroup() {
return <PlanRadioCards variant="bordered" />;
}
This example also needs: Field, RadioGroup.
Trailing control
Put the content first to move the checkbox to the trailing edge.
'use client';
import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
type ChoiceCardProps,
ChoiceCardTitle,
} from '@/components/nxds/components/choice-card/choice-card';
type ChoiceCardVariant = ChoiceCardProps['variant'];
interface CheckboxChoiceCardProps {
defaultChecked?: boolean;
description: string;
disabled?: boolean;
id: string;
invalid?: boolean;
title: string;
trailing?: boolean;
variant?: ChoiceCardVariant;
}
function CheckboxChoiceCard({
defaultChecked,
description,
disabled,
id,
invalid,
title,
trailing,
variant,
}: CheckboxChoiceCardProps) {
const checkbox = (
<Checkbox
id={id}
defaultChecked={defaultChecked}
disabled={disabled}
aria-invalid={invalid || undefined}
aria-labelledby={`${id}-title`}
aria-describedby={`${id}-description`}
/>
);
const content = (
<ChoiceCardContent>
<ChoiceCardTitle id={`${id}-title`}>{title}</ChoiceCardTitle>
<ChoiceCardDescription id={`${id}-description`}>
{description}
</ChoiceCardDescription>
</ChoiceCardContent>
);
return (
<ChoiceCard htmlFor={id} variant={variant}>
{trailing ? content : checkbox}
{trailing ? checkbox : content}
</ChoiceCard>
);
}
export default function ChoiceCardTrailingControl() {
return (
<div className="nx:w-96 nx:max-w-full">
<CheckboxChoiceCard
id="choice-card-trailing"
defaultChecked
trailing
variant="bordered"
title="Billing notices"
description="Invoices, receipts, and payment updates for every workspace seat in this account."
/>
</div>
);
}
Disabled
Disable the nested control to lock the choice and dim the card.
'use client';
import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
type ChoiceCardProps,
ChoiceCardTitle,
} from '@/components/nxds/components/choice-card/choice-card';
import {
RadioGroup,
RadioGroupItem,
} from '@/components/nxds/components/radio-group/radio-group';
type ChoiceCardVariant = ChoiceCardProps['variant'];
interface CheckboxChoiceCardProps {
defaultChecked?: boolean;
description: string;
disabled?: boolean;
id: string;
invalid?: boolean;
title: string;
trailing?: boolean;
variant?: ChoiceCardVariant;
}
function CheckboxChoiceCard({
defaultChecked,
description,
disabled,
id,
invalid,
title,
trailing,
variant,
}: CheckboxChoiceCardProps) {
const checkbox = (
<Checkbox
id={id}
defaultChecked={defaultChecked}
disabled={disabled}
aria-invalid={invalid || undefined}
aria-labelledby={`${id}-title`}
aria-describedby={`${id}-description`}
/>
);
const content = (
<ChoiceCardContent>
<ChoiceCardTitle id={`${id}-title`}>{title}</ChoiceCardTitle>
<ChoiceCardDescription id={`${id}-description`}>
{description}
</ChoiceCardDescription>
</ChoiceCardContent>
);
return (
<ChoiceCard htmlFor={id} variant={variant}>
{trailing ? content : checkbox}
{trailing ? checkbox : content}
</ChoiceCard>
);
}
interface RadioChoiceCardProps {
defaultValue?: string;
description: string;
disabled?: boolean;
id: string;
invalid?: boolean;
title: string;
trailing?: boolean;
variant?: ChoiceCardVariant;
}
function RadioChoiceCard({
defaultValue,
description,
disabled,
id,
invalid,
title,
trailing,
variant,
}: RadioChoiceCardProps) {
const item = (
<RadioGroupItem
id={id}
value={id}
disabled={disabled}
aria-invalid={invalid || undefined}
aria-labelledby={`${id}-title`}
aria-describedby={`${id}-description`}
/>
);
const content = (
<ChoiceCardContent>
<ChoiceCardTitle id={`${id}-title`}>{title}</ChoiceCardTitle>
<ChoiceCardDescription id={`${id}-description`}>
{description}
</ChoiceCardDescription>
</ChoiceCardContent>
);
return (
<RadioGroup aria-label={title} defaultValue={defaultValue ?? id}>
<ChoiceCard htmlFor={id} variant={variant}>
{trailing ? content : item}
{trailing ? item : content}
</ChoiceCard>
</RadioGroup>
);
}
export default function ChoiceCardDisabled() {
return (
<div className="nx:grid nx:w-96 nx:max-w-full nx:gap-3">
<CheckboxChoiceCard
id="choice-card-disabled"
disabled
title="Locked notifications"
description="This setting is managed by your workspace."
/>
<CheckboxChoiceCard
id="choice-card-disabled-checked"
disabled
defaultChecked
title="Usage reports"
description="Always on for workspace admins."
/>
<CheckboxChoiceCard
id="choice-card-disabled-invalid"
disabled
invalid
title="Audit log export"
description="Unavailable until your billing details are fixed."
/>
<RadioChoiceCard
id="choice-card-disabled-radio"
disabled
title="Enterprise plan"
description="Contact sales to switch to this plan."
/>
</div>
);
}
This example also needs: RadioGroup.
Invalid
Set aria-invalid on the nested control to give the card an error border.
'use client';
import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
type ChoiceCardProps,
ChoiceCardTitle,
} from '@/components/nxds/components/choice-card/choice-card';
import {
RadioGroup,
RadioGroupItem,
} from '@/components/nxds/components/radio-group/radio-group';
type ChoiceCardVariant = ChoiceCardProps['variant'];
interface CheckboxChoiceCardProps {
defaultChecked?: boolean;
description: string;
disabled?: boolean;
id: string;
invalid?: boolean;
title: string;
trailing?: boolean;
variant?: ChoiceCardVariant;
}
function CheckboxChoiceCard({
defaultChecked,
description,
disabled,
id,
invalid,
title,
trailing,
variant,
}: CheckboxChoiceCardProps) {
const checkbox = (
<Checkbox
id={id}
defaultChecked={defaultChecked}
disabled={disabled}
aria-invalid={invalid || undefined}
aria-labelledby={`${id}-title`}
aria-describedby={`${id}-description`}
/>
);
const content = (
<ChoiceCardContent>
<ChoiceCardTitle id={`${id}-title`}>{title}</ChoiceCardTitle>
<ChoiceCardDescription id={`${id}-description`}>
{description}
</ChoiceCardDescription>
</ChoiceCardContent>
);
return (
<ChoiceCard htmlFor={id} variant={variant}>
{trailing ? content : checkbox}
{trailing ? checkbox : content}
</ChoiceCard>
);
}
interface RadioChoiceCardProps {
defaultValue?: string;
description: string;
disabled?: boolean;
id: string;
invalid?: boolean;
title: string;
trailing?: boolean;
variant?: ChoiceCardVariant;
}
function RadioChoiceCard({
defaultValue,
description,
disabled,
id,
invalid,
title,
trailing,
variant,
}: RadioChoiceCardProps) {
const item = (
<RadioGroupItem
id={id}
value={id}
disabled={disabled}
aria-invalid={invalid || undefined}
aria-labelledby={`${id}-title`}
aria-describedby={`${id}-description`}
/>
);
const content = (
<ChoiceCardContent>
<ChoiceCardTitle id={`${id}-title`}>{title}</ChoiceCardTitle>
<ChoiceCardDescription id={`${id}-description`}>
{description}
</ChoiceCardDescription>
</ChoiceCardContent>
);
return (
<RadioGroup aria-label={title} defaultValue={defaultValue ?? id}>
<ChoiceCard htmlFor={id} variant={variant}>
{trailing ? content : item}
{trailing ? item : content}
</ChoiceCard>
</RadioGroup>
);
}
export default function ChoiceCardInvalid() {
return (
<div className="nx:grid nx:w-96 nx:max-w-full nx:gap-3">
<CheckboxChoiceCard
id="choice-card-invalid-unselected"
title="Product updates"
description="News about features and improvements."
/>
<CheckboxChoiceCard
id="choice-card-invalid-selected"
defaultChecked
title="Security alerts"
description="Sign-ins from new devices and password changes."
/>
<CheckboxChoiceCard
id="choice-card-invalid"
defaultChecked
invalid
title="Required policy"
description="Resolve this selection before continuing."
/>
<RadioChoiceCard
id="choice-card-invalid-radio"
invalid
title="Annual billing"
description="Choose a billing period to continue."
/>
</div>
);
}
This example also needs: RadioGroup.
Indeterminate
Show a partly selected checkbox card with checked="indeterminate".
'use client';
import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
type ChoiceCardProps,
ChoiceCardTitle,
} from '@/components/nxds/components/choice-card/choice-card';
type ChoiceCardVariant = ChoiceCardProps['variant'];
interface CheckboxChoiceCardProps {
defaultChecked?: boolean;
description: string;
disabled?: boolean;
id: string;
invalid?: boolean;
title: string;
trailing?: boolean;
variant?: ChoiceCardVariant;
}
function CheckboxChoiceCard({
defaultChecked,
description,
disabled,
id,
invalid,
title,
trailing,
variant,
}: CheckboxChoiceCardProps) {
const checkbox = (
<Checkbox
id={id}
defaultChecked={defaultChecked}
disabled={disabled}
aria-invalid={invalid || undefined}
aria-labelledby={`${id}-title`}
aria-describedby={`${id}-description`}
/>
);
const content = (
<ChoiceCardContent>
<ChoiceCardTitle id={`${id}-title`}>{title}</ChoiceCardTitle>
<ChoiceCardDescription id={`${id}-description`}>
{description}
</ChoiceCardDescription>
</ChoiceCardContent>
);
return (
<ChoiceCard htmlFor={id} variant={variant}>
{trailing ? content : checkbox}
{trailing ? checkbox : content}
</ChoiceCard>
);
}
export default function ChoiceCardIndeterminate() {
return (
<div className="nx:grid nx:w-96 nx:max-w-full nx:gap-3">
<CheckboxChoiceCard
id="choice-card-indeterminate-unselected"
title="Marketing"
description="Newsletters and event invitations."
/>
<CheckboxChoiceCard
id="choice-card-indeterminate-checked"
defaultChecked
title="Account"
description="Billing receipts and security notices."
/>
<ChoiceCard htmlFor="choice-card-indeterminate">
<Checkbox
id="choice-card-indeterminate"
checked="indeterminate"
aria-labelledby="choice-card-indeterminate-title"
aria-describedby="choice-card-indeterminate-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-indeterminate-title">
Product
</ChoiceCardTitle>
<ChoiceCardDescription id="choice-card-indeterminate-description">
Some of these notifications are on.
</ChoiceCardDescription>
</ChoiceCardContent>
</ChoiceCard>
</div>
);
}
All variants
Compare bordered and borderless cards for checkboxes and radios.
'use client';
import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
type ChoiceCardProps,
ChoiceCardTitle,
} from '@/components/nxds/components/choice-card/choice-card';
import {
RadioGroup,
RadioGroupItem,
} from '@/components/nxds/components/radio-group/radio-group';
type ChoiceCardVariant = ChoiceCardProps['variant'];
interface CheckboxChoiceCardProps {
defaultChecked?: boolean;
description: string;
disabled?: boolean;
id: string;
invalid?: boolean;
title: string;
trailing?: boolean;
variant?: ChoiceCardVariant;
}
function CheckboxChoiceCard({
defaultChecked,
description,
disabled,
id,
invalid,
title,
trailing,
variant,
}: CheckboxChoiceCardProps) {
const checkbox = (
<Checkbox
id={id}
defaultChecked={defaultChecked}
disabled={disabled}
aria-invalid={invalid || undefined}
aria-labelledby={`${id}-title`}
aria-describedby={`${id}-description`}
/>
);
const content = (
<ChoiceCardContent>
<ChoiceCardTitle id={`${id}-title`}>{title}</ChoiceCardTitle>
<ChoiceCardDescription id={`${id}-description`}>
{description}
</ChoiceCardDescription>
</ChoiceCardContent>
);
return (
<ChoiceCard htmlFor={id} variant={variant}>
{trailing ? content : checkbox}
{trailing ? checkbox : content}
</ChoiceCard>
);
}
interface RadioChoiceCardProps {
defaultValue?: string;
description: string;
disabled?: boolean;
id: string;
invalid?: boolean;
title: string;
trailing?: boolean;
variant?: ChoiceCardVariant;
}
function RadioChoiceCard({
defaultValue,
description,
disabled,
id,
invalid,
title,
trailing,
variant,
}: RadioChoiceCardProps) {
const item = (
<RadioGroupItem
id={id}
value={id}
disabled={disabled}
aria-invalid={invalid || undefined}
aria-labelledby={`${id}-title`}
aria-describedby={`${id}-description`}
/>
);
const content = (
<ChoiceCardContent>
<ChoiceCardTitle id={`${id}-title`}>{title}</ChoiceCardTitle>
<ChoiceCardDescription id={`${id}-description`}>
{description}
</ChoiceCardDescription>
</ChoiceCardContent>
);
return (
<RadioGroup aria-label={title} defaultValue={defaultValue ?? id}>
<ChoiceCard htmlFor={id} variant={variant}>
{trailing ? content : item}
{trailing ? item : content}
</ChoiceCard>
</RadioGroup>
);
}
export default function ChoiceCardAllVariants() {
return (
<div className="nx:grid nx:w-[min(100%,56rem)] nx:grid-cols-1 nx:gap-6 nx:md:grid-cols-2">
<div className="nx:grid nx:gap-3">
<CheckboxChoiceCard
id="choice-card-checkbox-bordered"
title="Checkbox bordered"
description="Default rich option card with a visible frame."
/>
<CheckboxChoiceCard
id="choice-card-checkbox-borderless"
variant="borderless"
title="Checkbox borderless"
description="Quiet option card with no resting frame."
/>
</div>
<div className="nx:grid nx:gap-3">
<RadioChoiceCard
id="choice-card-radio-bordered"
title="Radio bordered"
description="Radio choices use the same card shell."
/>
<RadioChoiceCard
id="choice-card-radio-borderless"
variant="borderless"
trailing
title="Radio borderless"
description="Trailing controls use child order and top alignment."
/>
</div>
</div>
);
}
This example also needs: RadioGroup.
Props
ChoiceCard
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "borderless" | "bordered" | null | "bordered" | —No description |
ChoiceCardContent
ChoiceCardContent adds no props of its own; it accepts the props of the element or primitive it wraps.
ChoiceCardDescription
ChoiceCardDescription adds no props of its own; it accepts the props of the element or primitive it wraps.
ChoiceCardTitle
ChoiceCardTitle adds no props of its own; it accepts the props of the element or primitive it wraps.
ChoiceCard is a clickable card for a checkbox or radio choice with a title and description. The nested Checkbox or RadioGroupItem stays the only control. For a simple inline label use Label, and for compact options use ChoiceRow.
<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
ChoiceCardTitle,
} from '@/components/choice-card';
</script>
<template>
<div class="nx:w-96 nx:max-w-full">
<ChoiceCard for="choice-card-default" variant="bordered">
<Checkbox
id="choice-card-default"
aria-labelledby="choice-card-default-title"
aria-describedby="choice-card-default-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-default-title"
>Product updates</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-default-description"
>News about features and improvements.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
</div>
</template>
This example also needs: Checkbox.
Installation
- 1
Install these components first:
- 2
Install the following dependencies:
npm install class-variance-authority@^0.7.1 - 3
Copy and paste the following code into your project.
components/choice-card/ChoiceCard.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'; import { type ChoiceCardVariants, choiceCardVariants } from '.'; export interface ChoiceCardProps extends Omit<RekaLabelProps, 'asChild'> { variant?: ChoiceCardVariants['variant']; class?: HTMLAttributes['class']; } const props = withDefaults(defineProps<ChoiceCardProps>(), { variant: 'bordered', }); const delegated = reactiveOmit(props, 'class', 'variant'); </script> <template> <Label v-bind="delegated" data-slot="choice-card" :data-variant="variant" :class="cn(choiceCardVariants({ variant }), props.class)" > <slot /> </Label> </template>components/choice-card/ChoiceCardContent.vue <script setup lang="ts"> import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface ChoiceCardContentProps { class?: HTMLAttributes['class']; } const props = defineProps<ChoiceCardContentProps>(); </script> <template> <div data-slot="choice-card-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-card/ChoiceCardDescription.vue <script setup lang="ts"> import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface ChoiceCardDescriptionProps { class?: HTMLAttributes['class']; } const props = defineProps<ChoiceCardDescriptionProps>(); </script> <template> <p data-slot="choice-card-description" :class=" cn( 'nx:m-0 nx:min-w-0 nx:break-words nx:typography-body-default nx:text-muted-foreground', 'nx:group-has-[>[data-slot=checkbox]:disabled]/choice-card:text-disabled-foreground', 'nx:group-has-[>[data-slot=radio-group-item]:disabled]/choice-card:text-disabled-foreground', props.class ) " > <slot /> </p> </template>components/choice-card/ChoiceCardTitle.vue <script setup lang="ts"> import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface ChoiceCardTitleProps { class?: HTMLAttributes['class']; } const props = defineProps<ChoiceCardTitleProps>(); </script> <template> <div data-slot="choice-card-title" :class=" cn( 'nx:min-w-0 nx:break-words nx:typography-label-default nx:text-foreground', 'nx:group-has-[>[data-slot=checkbox]:disabled]/choice-card:text-disabled-foreground', 'nx:group-has-[>[data-slot=radio-group-item]:disabled]/choice-card:text-disabled-foreground', props.class ) " > <slot /> </div> </template>components/choice-card/index.ts import { cva, type VariantProps } from 'class-variance-authority'; export { default as ChoiceCard, type ChoiceCardProps } from './ChoiceCard.vue'; export { default as ChoiceCardContent, type ChoiceCardContentProps, } from './ChoiceCardContent.vue'; export { default as ChoiceCardDescription, type ChoiceCardDescriptionProps, } from './ChoiceCardDescription.vue'; export { default as ChoiceCardTitle, type ChoiceCardTitleProps, } from './ChoiceCardTitle.vue'; export const choiceCardVariants = cva( [ 'nx:group/choice-card nx:box-border nx:flex nx:min-w-0 nx:w-full nx:cursor-pointer nx:items-start nx:gap-3 nx:rounded-md nx:bg-background nx:p-4 nx:transition-colors', 'nx:[&>[data-slot=checkbox]]:mt-0.5 nx:[&>[data-slot=radio-group-item]]:mt-0.5', 'nx:not-has-[>[data-slot=checkbox]:disabled]:not-has-[>[data-slot=radio-group-item]:disabled]:hover:bg-background-hover', 'nx:not-has-[>[data-slot=checkbox]:disabled]:not-has-[>[data-slot=radio-group-item]:disabled]:active:bg-background-active', 'nx:has-[>[data-slot=checkbox]:disabled]:cursor-not-allowed nx:has-[>[data-slot=checkbox]:disabled]:border-border-disabled nx:has-[>[data-slot=checkbox]:disabled]:bg-disabled', 'nx:has-[>[data-slot=radio-group-item]:disabled]:cursor-not-allowed nx:has-[>[data-slot=radio-group-item]:disabled]:border-border-disabled nx:has-[>[data-slot=radio-group-item]:disabled]:bg-disabled', 'nx:not-has-[>[data-slot=checkbox]:disabled]:not-has-[>[data-slot=radio-group-item]:disabled]:has-[>[data-slot=checkbox][aria-invalid=true]]:border-error-border', 'nx:not-has-[>[data-slot=checkbox]:disabled]:not-has-[>[data-slot=radio-group-item]:disabled]:has-[>[data-slot=radio-group-item][aria-invalid=true]]:border-error-border', 'nx:not-has-[>[data-slot=checkbox]:disabled]:not-has-[>[data-slot=radio-group-item]:disabled]:not-has-[>[data-slot=checkbox][aria-invalid=true]]:not-has-[>[data-slot=radio-group-item][aria-invalid=true]]:has-[>[data-slot=checkbox][data-state=checked]]:border-primary-border', 'nx:not-has-[>[data-slot=checkbox]:disabled]:not-has-[>[data-slot=radio-group-item]:disabled]:not-has-[>[data-slot=checkbox][aria-invalid=true]]:not-has-[>[data-slot=radio-group-item][aria-invalid=true]]:has-[>[data-slot=checkbox][data-state=checked]]:bg-primary-subtle', 'nx:not-has-[>[data-slot=checkbox]:disabled]:not-has-[>[data-slot=radio-group-item]:disabled]:not-has-[>[data-slot=checkbox][aria-invalid=true]]:not-has-[>[data-slot=radio-group-item][aria-invalid=true]]:has-[>[data-slot=checkbox][data-state=indeterminate]]:border-primary-border', 'nx:not-has-[>[data-slot=checkbox]:disabled]:not-has-[>[data-slot=radio-group-item]:disabled]:not-has-[>[data-slot=checkbox][aria-invalid=true]]:not-has-[>[data-slot=radio-group-item][aria-invalid=true]]:has-[>[data-slot=checkbox][data-state=indeterminate]]:bg-primary-subtle', 'nx:not-has-[>[data-slot=checkbox]:disabled]:not-has-[>[data-slot=radio-group-item]:disabled]:not-has-[>[data-slot=checkbox][aria-invalid=true]]:not-has-[>[data-slot=radio-group-item][aria-invalid=true]]:has-[>[data-slot=radio-group-item][data-state=checked]]:border-primary-border', 'nx:not-has-[>[data-slot=checkbox]:disabled]:not-has-[>[data-slot=radio-group-item]:disabled]:not-has-[>[data-slot=checkbox][aria-invalid=true]]:not-has-[>[data-slot=radio-group-item][aria-invalid=true]]:has-[>[data-slot=radio-group-item][data-state=checked]]:bg-primary-subtle', ], { variants: { variant: { bordered: 'nx:border-default nx:border-border-default', borderless: 'nx:border-default nx:border-transparent', }, }, defaultVariants: { variant: 'bordered', }, } ); export type ChoiceCardVariants = VariantProps<typeof choiceCardVariants>; - 4
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 { ChoiceCard, ChoiceCardContent, ChoiceCardDescription, ChoiceCardTitle } from '@/components/choice-card';
Examples
With radio group
Pick one plan from a RadioGroup of cards.
<script setup lang="ts">
import { useId } from 'vue';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
ChoiceCardTitle,
} from '@/components/choice-card';
import { FieldLegend, FieldSet } from '@/components/field';
import { RadioGroup, RadioGroupItem } from '@/components/radio-group';
const legendId = useId();
</script>
<template>
<FieldSet class="nx:w-96 nx:max-w-full">
<FieldLegend :id="legendId">Plan</FieldLegend>
<RadioGroup :aria-labelledby="legendId">
<ChoiceCard for="choice-card-radio-standard" variant="bordered">
<RadioGroupItem
id="choice-card-radio-standard"
value="standard"
aria-labelledby="choice-card-radio-standard-title"
aria-describedby="choice-card-radio-standard-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-radio-standard-title"
>Standard</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-radio-standard-description"
>Best for most teams.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
<ChoiceCard for="choice-card-radio-priority" variant="bordered">
<RadioGroupItem
id="choice-card-radio-priority"
value="priority"
aria-labelledby="choice-card-radio-priority-title"
aria-describedby="choice-card-radio-priority-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-radio-priority-title"
>Priority</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-radio-priority-description"
>Faster response routing for urgent teams.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
</RadioGroup>
</FieldSet>
</template>
This example also needs: Field, RadioGroup.
Trailing control
Put the content first to move the checkbox to the trailing edge.
<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
ChoiceCardTitle,
} from '@/components/choice-card';
</script>
<template>
<div class="nx:w-96 nx:max-w-full">
<ChoiceCard for="choice-card-trailing" variant="bordered">
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-trailing-title"
>Billing notices</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-trailing-description"
>Invoices, receipts, and payment updates for every workspace seat in
this account.</ChoiceCardDescription
>
</ChoiceCardContent>
<Checkbox
id="choice-card-trailing"
:default-value="true"
aria-labelledby="choice-card-trailing-title"
aria-describedby="choice-card-trailing-description"
/>
</ChoiceCard>
</div>
</template>
Disabled
Disable the nested control to lock the choice and dim the card.
<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
ChoiceCardTitle,
} from '@/components/choice-card';
import { RadioGroup, RadioGroupItem } from '@/components/radio-group';
</script>
<template>
<div class="nx:grid nx:w-96 nx:max-w-full nx:gap-3">
<ChoiceCard for="choice-card-disabled">
<Checkbox
id="choice-card-disabled"
disabled
aria-labelledby="choice-card-disabled-title"
aria-describedby="choice-card-disabled-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-disabled-title"
>Locked notifications</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-disabled-description"
>This setting is managed by your workspace.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
<ChoiceCard for="choice-card-disabled-checked">
<Checkbox
id="choice-card-disabled-checked"
:default-value="true"
disabled
aria-labelledby="choice-card-disabled-checked-title"
aria-describedby="choice-card-disabled-checked-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-disabled-checked-title"
>Usage reports</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-disabled-checked-description"
>Always on for workspace admins.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
<ChoiceCard for="choice-card-disabled-invalid">
<Checkbox
id="choice-card-disabled-invalid"
disabled
aria-invalid="true"
aria-labelledby="choice-card-disabled-invalid-title"
aria-describedby="choice-card-disabled-invalid-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-disabled-invalid-title"
>Audit log export</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-disabled-invalid-description"
>Unavailable until your billing details are
fixed.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
<RadioGroup
aria-label="Enterprise plan"
default-value="choice-card-disabled-radio"
>
<ChoiceCard for="choice-card-disabled-radio">
<RadioGroupItem
id="choice-card-disabled-radio"
value="choice-card-disabled-radio"
disabled
aria-labelledby="choice-card-disabled-radio-title"
aria-describedby="choice-card-disabled-radio-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-disabled-radio-title"
>Enterprise plan</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-disabled-radio-description"
>Contact sales to switch to this plan.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
</RadioGroup>
</div>
</template>
This example also needs: RadioGroup.
Invalid
Set aria-invalid on the nested control to give the card an error border.
<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
ChoiceCardTitle,
} from '@/components/choice-card';
import { RadioGroup, RadioGroupItem } from '@/components/radio-group';
</script>
<template>
<div class="nx:grid nx:w-96 nx:max-w-full nx:gap-3">
<ChoiceCard for="choice-card-invalid-unselected">
<Checkbox
id="choice-card-invalid-unselected"
aria-labelledby="choice-card-invalid-unselected-title"
aria-describedby="choice-card-invalid-unselected-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-invalid-unselected-title"
>Product updates</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-invalid-unselected-description"
>News about features and improvements.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
<ChoiceCard for="choice-card-invalid-selected">
<Checkbox
id="choice-card-invalid-selected"
:default-value="true"
aria-labelledby="choice-card-invalid-selected-title"
aria-describedby="choice-card-invalid-selected-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-invalid-selected-title"
>Security alerts</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-invalid-selected-description"
>Sign-ins from new devices and password
changes.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
<ChoiceCard for="choice-card-invalid">
<Checkbox
id="choice-card-invalid"
:default-value="true"
aria-invalid="true"
aria-labelledby="choice-card-invalid-title"
aria-describedby="choice-card-invalid-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-invalid-title"
>Required policy</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-invalid-description"
>Resolve this selection before continuing.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
<RadioGroup
aria-label="Annual billing"
default-value="choice-card-invalid-radio"
>
<ChoiceCard for="choice-card-invalid-radio">
<RadioGroupItem
id="choice-card-invalid-radio"
value="choice-card-invalid-radio"
aria-invalid="true"
aria-labelledby="choice-card-invalid-radio-title"
aria-describedby="choice-card-invalid-radio-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-invalid-radio-title"
>Annual billing</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-invalid-radio-description"
>Choose a billing period to continue.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
</RadioGroup>
</div>
</template>
This example also needs: RadioGroup.
Indeterminate
Show a partly selected checkbox card with model-value="indeterminate".
<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
ChoiceCardTitle,
} from '@/components/choice-card';
</script>
<template>
<div class="nx:grid nx:w-96 nx:max-w-full nx:gap-3">
<ChoiceCard for="choice-card-indeterminate-unselected">
<Checkbox
id="choice-card-indeterminate-unselected"
aria-labelledby="choice-card-indeterminate-unselected-title"
aria-describedby="choice-card-indeterminate-unselected-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-indeterminate-unselected-title"
>Marketing</ChoiceCardTitle
>
<ChoiceCardDescription
id="choice-card-indeterminate-unselected-description"
>Newsletters and event invitations.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
<ChoiceCard for="choice-card-indeterminate-checked">
<Checkbox
id="choice-card-indeterminate-checked"
:default-value="true"
aria-labelledby="choice-card-indeterminate-checked-title"
aria-describedby="choice-card-indeterminate-checked-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-indeterminate-checked-title"
>Account</ChoiceCardTitle
>
<ChoiceCardDescription
id="choice-card-indeterminate-checked-description"
>Billing receipts and security notices.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
<ChoiceCard for="choice-card-indeterminate">
<Checkbox
id="choice-card-indeterminate"
model-value="indeterminate"
aria-labelledby="choice-card-indeterminate-title"
aria-describedby="choice-card-indeterminate-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-indeterminate-title"
>Product</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-indeterminate-description"
>Some of these notifications are on.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
</div>
</template>
All variants
Compare bordered and borderless cards for checkboxes and radios.
<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
import {
ChoiceCard,
ChoiceCardContent,
ChoiceCardDescription,
ChoiceCardTitle,
} from '@/components/choice-card';
import { RadioGroup, RadioGroupItem } from '@/components/radio-group';
</script>
<template>
<div
class="nx:grid nx:w-[min(100%,56rem)] nx:grid-cols-1 nx:gap-6 nx:md:grid-cols-2"
>
<div class="nx:grid nx:gap-3">
<ChoiceCard for="choice-card-checkbox-bordered">
<Checkbox
id="choice-card-checkbox-bordered"
aria-labelledby="choice-card-checkbox-bordered-title"
aria-describedby="choice-card-checkbox-bordered-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-checkbox-bordered-title"
>Checkbox bordered</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-checkbox-bordered-description"
>Default rich option card with a visible
frame.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
<ChoiceCard for="choice-card-checkbox-borderless" variant="borderless">
<Checkbox
id="choice-card-checkbox-borderless"
aria-labelledby="choice-card-checkbox-borderless-title"
aria-describedby="choice-card-checkbox-borderless-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-checkbox-borderless-title"
>Checkbox borderless</ChoiceCardTitle
>
<ChoiceCardDescription
id="choice-card-checkbox-borderless-description"
>Quiet option card with no resting frame.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
</div>
<div class="nx:grid nx:gap-3">
<RadioGroup
aria-label="Radio bordered"
default-value="choice-card-radio-bordered"
>
<ChoiceCard for="choice-card-radio-bordered">
<RadioGroupItem
id="choice-card-radio-bordered"
value="choice-card-radio-bordered"
aria-labelledby="choice-card-radio-bordered-title"
aria-describedby="choice-card-radio-bordered-description"
/>
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-radio-bordered-title"
>Radio bordered</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-radio-bordered-description"
>Radio choices use the same card shell.</ChoiceCardDescription
>
</ChoiceCardContent>
</ChoiceCard>
</RadioGroup>
<RadioGroup
aria-label="Radio borderless"
default-value="choice-card-radio-borderless"
>
<ChoiceCard for="choice-card-radio-borderless" variant="borderless">
<ChoiceCardContent>
<ChoiceCardTitle id="choice-card-radio-borderless-title"
>Radio borderless</ChoiceCardTitle
>
<ChoiceCardDescription id="choice-card-radio-borderless-description"
>Trailing controls use child order and top
alignment.</ChoiceCardDescription
>
</ChoiceCardContent>
<RadioGroupItem
id="choice-card-radio-borderless"
value="choice-card-radio-borderless"
aria-labelledby="choice-card-radio-borderless-title"
aria-describedby="choice-card-radio-borderless-description"
/>
</ChoiceCard>
</RadioGroup>
</div>
</div>
</template>
This example also needs: RadioGroup.
Props
ChoiceCard
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "bordered" | "borderless" | null | "bordered" | —No description |
ChoiceCardContent
ChoiceCardContent adds no props of its own; it accepts the props of the element or primitive it wraps.
ChoiceCardDescription
ChoiceCardDescription adds no props of its own; it accepts the props of the element or primitive it wraps.
ChoiceCardTitle
ChoiceCardTitle adds no props of its own; it accepts the props of the element or primitive it wraps.