Skip to main content

Overview

The Choicebox component provides a card-based interface for making selections. It comes in two variants:
  • Choicebox: Acts as a checkbox for multiple selections
  • ChoiceboxRadio: Acts as a radio button for single selections within a group
Built on React Aria Components, Choicebox ensures full accessibility with keyboard navigation, focus management, and screen reader support.

Installation

npm install react-aria-components class-variance-authority lucide-react

Import

import { Choicebox, ChoiceboxRadio } from '@stride-ui/components';
import { RadioGroup } from '@stride-ui/components';

Basic Usage

Checkbox Variant

Use the Choicebox component for multiple independent selections:
import { Choicebox } from '@stride-ui/components';

export default function Example() {
  return (
    <Choicebox
      title="Basic Plan"
      description="Perfect for individuals getting started with our platform."
    />
  );
}

Radio Variant

Use ChoiceboxRadio within a RadioGroup for single selections:
import { ChoiceboxRadio } from '@stride-ui/components';
import { RadioGroup } from '@stride-ui/components';

export default function Example() {
  return (
    <RadioGroup label="Choose your plan">
      <ChoiceboxRadio
        value="basic"
        title="Basic Plan"
        description="Perfect for individuals getting started."
      />
      <ChoiceboxRadio
        value="pro"
        title="Pro Plan"
        description="Great for small teams and growing businesses."
      />
      <ChoiceboxRadio
        value="enterprise"
        title="Enterprise Plan"
        description="Complete solution for large organizations."
      />
    </RadioGroup>
  );
}

Props

Choicebox Props

title
React.ReactNode
The main heading text displayed in the choicebox card.
description
React.ReactNode
Secondary text providing additional context or details about the option.
size
'sm' | 'md' | 'lg'
default:"'md'"
Controls the padding and overall size of the choicebox card.
  • sm: Compact size with p-3
  • md: Default size with p-4
  • lg: Large size with p-6
isSelected
boolean
Controls whether the checkbox is checked. Use for controlled components.
isIndeterminate
boolean
Displays a minus icon instead of a checkmark, indicating a partial selection state.
isDisabled
boolean
default:"false"
Disables the choicebox, preventing user interaction and applying disabled styling.
onChange
(isSelected: boolean) => void
Callback fired when the selection state changes. Receives the new selected state.
className
string
Additional CSS classes to apply to the choicebox container.
value
string
The value associated with this checkbox (inherited from React Aria).

ChoiceboxRadio Props

title
React.ReactNode
The main heading text displayed in the choicebox card.
description
React.ReactNode
Secondary text providing additional context or details about the option.
size
'sm' | 'md' | 'lg'
default:"'md'"
Controls the padding and overall size of the choicebox card.
value
string
required
The unique value for this radio option within the group.
isDisabled
boolean
default:"false"
Disables the radio option, preventing user interaction.
className
string
Additional CSS classes to apply to the choicebox container.

Examples

Sizes

Choicebox supports three size variants:
import { Choicebox } from '@stride-ui/components';

export default function SizesExample() {
  return (
    <div className="flex flex-col space-y-4 max-w-sm">
      <Choicebox
        size="sm"
        title="Small Plan"
        description="Compact option for basic needs."
      />
      <Choicebox
        size="md"
        title="Medium Plan"
        description="Standard option with balanced features and pricing."
      />
      <Choicebox
        size="lg"
        title="Large Plan"
        description="Comprehensive solution with all features included."
      />
    </div>
  );
}

Title Only

When no description is provided, the title is displayed alone:
import { Choicebox } from '@stride-ui/components';

export default function TitleOnlyExample() {
  return (
    <Choicebox title="Enterprise Plan" />
  );
}

States

Choicebox supports multiple states including selected, indeterminate, and disabled:
import { Choicebox } from '@stride-ui/components';

export default function StatesExample() {
  return (
    <div className="flex flex-col space-y-4 max-w-sm">
      <Choicebox
        title="Unchecked"
        description="This option is not selected."
      />
      <Choicebox
        title="Selected"
        description="This option is currently selected."
        isSelected
      />
      <Choicebox
        title="Indeterminate"
        description="This option has a partial selection state."
        isIndeterminate
      />
      <Choicebox
        title="Disabled"
        description="This option is disabled and cannot be selected."
        isDisabled
      />
      <Choicebox
        title="Disabled & Selected"
        description="This option is disabled but was previously selected."
        isSelected
        isDisabled
      />
    </div>
  );
}

Controlled Checkbox Selection

Manage multiple independent selections with state:
import { useState } from 'react';
import { Choicebox } from '@stride-ui/components';

export default function ControlledExample() {
  const [features, setFeatures] = useState({
    notifications: false,
    analytics: true,
    api: false,
    support: false,
  });

  const updateFeature = (feature: keyof typeof features) => (selected: boolean) => {
    setFeatures(prev => ({ ...prev, [feature]: selected }));
  };

  return (
    <div className="flex flex-col space-y-4 max-w-md">
      <h3 className="font-semibold text-lg mb-2">Select Features</h3>
      <Choicebox
        title="Push Notifications"
        description="Receive real-time updates about your projects and team activities."
        isSelected={features.notifications}
        onChange={updateFeature('notifications')}
      />
      <Choicebox
        title="Advanced Analytics"
        description="Access detailed insights and reporting tools for better decision making."
        isSelected={features.analytics}
        onChange={updateFeature('analytics')}
      />
      <Choicebox
        title="API Access"
        description="Integrate with third-party tools and build custom solutions."
        isSelected={features.api}
        onChange={updateFeature('api')}
      />
      <Choicebox
        title="Priority Support"
        description="Get 24/7 support with faster response times and dedicated assistance."
        isSelected={features.support}
        onChange={updateFeature('support')}
      />
    </div>
  );
}

Controlled Radio Selection

Use RadioGroup to manage single selections:
import { useState } from 'react';
import { ChoiceboxRadio } from '@stride-ui/components';
import { RadioGroup } from '@stride-ui/components';

export default function RadioExample() {
  const [selectedPlan, setSelectedPlan] = useState('pro');

  return (
    <RadioGroup 
      value={selectedPlan} 
      onChange={setSelectedPlan}
      label="Choose your plan" 
      className="max-w-sm"
    >
      <ChoiceboxRadio
        value="free"
        title="Free Plan"
        description="Get started with basic features and limited usage."
      />
      <ChoiceboxRadio
        value="pro"
        title="Pro Plan"
        description="Perfect for professionals with advanced features and priority support."
      />
      <ChoiceboxRadio
        value="enterprise"
        title="Enterprise Plan"
        description="Complete solution for large organizations with custom integrations."
      />
    </RadioGroup>
  );
}

Long Content

Choicebox handles longer descriptions gracefully:
import { ChoiceboxRadio } from '@stride-ui/components';
import { RadioGroup } from '@stride-ui/components';

export default function LongContentExample() {
  return (
    <RadioGroup label="Select subscription" className="max-w-md">
      <ChoiceboxRadio
        value="basic"
        title="Basic Subscription"
        description="This is a basic subscription with limited features. You get access to core functionality, basic support, and standard updates. Perfect for individuals or small teams just getting started."
      />
      <ChoiceboxRadio
        value="premium"
        title="Premium Subscription"
        description="Our premium offering includes everything in Basic plus advanced analytics, priority support, custom integrations, and early access to new features. Ideal for growing businesses and professional teams."
      />
    </RadioGroup>
  );
}

Integration with Form Libraries

React Hook Form

import { useForm, Controller } from 'react-hook-form';
import { Choicebox, ChoiceboxRadio } from '@stride-ui/components';
import { RadioGroup } from '@stride-ui/components';

type FormData = {
  features: {
    notifications: boolean;
    analytics: boolean;
  };
  plan: string;
};

export default function FormExample() {
  const { control, handleSubmit } = useForm<FormData>({
    defaultValues: {
      features: {
        notifications: false,
        analytics: true,
      },
      plan: 'pro',
    },
  });

  const onSubmit = (data: FormData) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
      {/* Checkbox selections */}
      <div className="space-y-4">
        <h3 className="font-semibold">Features</h3>
        <Controller
          name="features.notifications"
          control={control}
          render={({ field }) => (
            <Choicebox
              title="Push Notifications"
              description="Receive real-time updates"
              isSelected={field.value}
              onChange={field.onChange}
            />
          )}
        />
        <Controller
          name="features.analytics"
          control={control}
          render={({ field }) => (
            <Choicebox
              title="Advanced Analytics"
              description="Access detailed insights"
              isSelected={field.value}
              onChange={field.onChange}
            />
          )}
        />
      </div>

      {/* Radio selection */}
      <Controller
        name="plan"
        control={control}
        render={({ field }) => (
          <RadioGroup 
            label="Choose your plan"
            value={field.value}
            onChange={field.onChange}
          >
            <ChoiceboxRadio
              value="basic"
              title="Basic Plan"
              description="Essential features"
            />
            <ChoiceboxRadio
              value="pro"
              title="Pro Plan"
              description="Advanced features"
            />
            <ChoiceboxRadio
              value="enterprise"
              title="Enterprise Plan"
              description="Complete solution"
            />
          </RadioGroup>
        )}
      />

      <button type="submit">Submit</button>
    </form>
  );
}

Formik

import { Formik, Form, Field } from 'formik';
import { Choicebox, ChoiceboxRadio } from '@stride-ui/components';
import { RadioGroup } from '@stride-ui/components';

export default function FormikExample() {
  return (
    <Formik
      initialValues={{
        notifications: false,
        analytics: true,
        plan: 'pro',
      }}
      onSubmit={(values) => {
        console.log(values);
      }}
    >
      {({ values, setFieldValue }) => (
        <Form className="space-y-6">
          <div className="space-y-4">
            <h3 className="font-semibold">Features</h3>
            <Choicebox
              title="Push Notifications"
              description="Receive real-time updates"
              isSelected={values.notifications}
              onChange={(selected) => setFieldValue('notifications', selected)}
            />
            <Choicebox
              title="Advanced Analytics"
              description="Access detailed insights"
              isSelected={values.analytics}
              onChange={(selected) => setFieldValue('analytics', selected)}
            />
          </div>

          <RadioGroup
            label="Choose your plan"
            value={values.plan}
            onChange={(value) => setFieldValue('plan', value)}
          >
            <ChoiceboxRadio value="basic" title="Basic Plan" />
            <ChoiceboxRadio value="pro" title="Pro Plan" />
            <ChoiceboxRadio value="enterprise" title="Enterprise Plan" />
          </RadioGroup>

          <button type="submit">Submit</button>
        </Form>
      )}
    </Formik>
  );
}

Accessibility

Choicebox is built on React Aria Components and includes comprehensive accessibility features:

Keyboard Navigation

  • Space: Toggle checkbox selection or select radio option
  • Tab: Move focus between choiceboxes
  • Shift + Tab: Move focus backwards
  • Arrow Keys: Navigate between radio options within a RadioGroup

Screen Readers

  • Proper ARIA labels and roles are automatically applied
  • Selection state is announced to screen readers
  • Disabled and indeterminate states are communicated
  • Focus management follows WCAG 2.1 guidelines

Focus Management

  • Clear focus indicators with ring styles
  • Focus-visible only shows ring for keyboard navigation
  • Focus state respects system preferences

Best Practices

  1. Always provide a title: The title acts as the label for screen readers
  2. Use RadioGroup for radio variants: Ensures proper ARIA relationships
  3. Provide meaningful descriptions: Helps users understand their options
  4. Use isDisabled appropriately: Don’t hide disabled options; show them with context
  5. Ensure sufficient color contrast: The component uses semantic tokens for proper contrast

Design Tokens

Choicebox uses the following design tokens from the Stride Design System:
  • --border-primary: Default border color
  • --border-secondary: Hover border color
  • --border-focus: Focus ring color
  • --bg-primary: Default background
  • --bg-secondary: Hover and selected background
  • --interactive-primary: Selected indicator color
  • --interactive-primary-hover: Hovered selected state
  • --text-primary: Title text color
  • --text-tertiary: Description text color
  • --radius-sm: Indicator border radius (checkbox)
  • --radius-md: Card border radius
  • --transition-normal: Transition duration

Component Structure

The Choicebox component is composed of:
  1. Card Container: The outer clickable area with border and background
  2. Indicator: Checkbox (square) or radio (circle) in the top-right corner
  3. Content Area: Title and optional description with responsive typography
  4. Visual Feedback: Hover, focus, and selection states
Source: /home/daytona/workspace/source/src/components/ui/Choicebox/Choicebox.tsx
  • Checkbox - Traditional checkbox for compact forms
  • Radio - Traditional radio buttons for lists
  • RadioGroup - Container for radio options

Build docs developers (and LLMs) love