Content

Radio Cards
v0.1.13
Documentation Under Review

A deprecated radio group component with card-style visual presentation. Use RadioGroup component for new implementations with better accessibility and simpler API.

Deprecated Component

This component is deprecated and should not be used in new projects. Please use the RadioGroup component instead with custom card styling, which provides better accessibility, simpler API, and improved functionality.

Installation

Install the RadioCards component from the Catalyst component library:

npm install @pmi/catalyst-radio-cards

Overview

The RadioCards component provides a card-style visual presentation for radio button groups, built on Radix UI's RadioGroup primitive. Each option is displayed as a selectable card with title and optional label text.

Migration Required: This component is deprecated. For new implementations, use RadioGroup with custom card-style CSS, which provides: - Simpler API with fewer required components - Better accessibility with automatic ARIA attributes - More flexible styling options - Improved keyboard navigation - Better state management

Key Features

  • Card-Style Selection: Visual cards instead of traditional radio buttons
  • Four Size Variants: xs, sm, md, lg for different contexts
  • Danger State: Visual warning for validation errors
  • Flexible Layout: Title and label can be arranged flexibly
  • Radix UI Foundation: Built on accessible RadioGroup primitive
  • Disabled State: Visual indication with opacity and pointer-events
  • Controlled State: Value and onValueChange for form integration

Examples

Basic Usage

Radio cards with title and label above.

Preview

Label Below Title

Flexible content ordering with label below title.

Preview

Title Only

Compact cards with only title text.

Preview

Size Variants

Four size options for different UI contexts.

Preview

Large

Medium (Default)

Small

Extra Small

Danger State

Use danger prop for validation errors or warning states.

Preview
Please select a valid option

Disabled State

Disabled state prevents user interaction.

Preview

API Reference

RadioCardsRoot

The root container managing radio group state and context.

Prop

Type

RadioCardsItem

Individual selectable card item within the radio group.

Prop

Type

RadioCardsTitle

Title text displayed prominently on the card (typically price or main value).

Prop

Type

RadioCardsLabel

Label text for additional context (typically plan name or option description).

Prop

Type

Size Specifications

The RadioCards component provides four size variants with responsive padding and typography:

Prop

Type

Border Width Compensation

When a card is selected, the border width increases from --border-xs to --border-md. To prevent layout shift, the padding is reduced:

  • Normal: Full padding with thin border
  • Selected: Reduced padding (compensates for thicker border)
  • Net Result: Card dimensions remain constant

Styling Details

State-Based Styling

The component uses data attributes for state-based styling:

// Size context from RadioCardsRoot
<RadioCardsRoot data-size="md" data-danger="false">
  {/* Items respond to parent context */}
  <RadioCardsItem data-state="checked">{/* Title and Label change colors based on state */}</RadioCardsItem>
</RadioCardsRoot>

Color Transitions

Colors change based on state:

  • Default: Primary text color with dark border
  • Hover: White text with filled background
  • Selected: Thicker border, maintained colors
  • Danger: Red text and border, white on hover
  • Disabled: Reduced opacity, no pointer events

Typography Scale

Title sizes scale with card size:

  • xs/sm: text-header-2xs
  • md: text-header-xs
  • lg: text-header-md

Label always uses text-body-sm regardless of card size.

Design Tokens

The RadioCards component uses Catalyst design tokens:

  • Colors:

    • Text: --text-primary, --text-white, --text-danger
    • Border: --border-off-black-dark, --border-danger
    • Fill: --fill-off-black, --fill-danger
  • Spacing:

    • Padding: --scale-8, --scale-12, --scale-16, --scale-20, --scale-24
    • Gap: --scale-2, --scale-4, --scale-8, --scale-12
  • Border:

    • Width: --border-xs (unchecked), --border-md (checked)
    • Radius: --rounded-xs
  • Effects:

    • Opacity: --opacity-disabled
    • Outline: --border-sm (focus ring)
    • Transitions: transition-colors

Accessibility

Limited Accessibility: This component has basic accessibility via Radix UI. Use RadioGroup for enhanced accessibility features.

Keyboard Navigation

  • Tab: Move focus between radio cards
  • Arrow Keys: Navigate between cards in the group
  • Space: Select focused card
  • Enter: Select focused card (if applicable)

ARIA Attributes

Provided automatically by Radix UI RadioGroup:

  • role="radiogroup" on root
  • role="radio" on items
  • aria-checked state on items
  • aria-disabled when disabled

WCAG 2.1 AA Compliance

  • Contrast Ratios: Text and borders meet 4.5:1 minimum
  • Touch Targets: All sizes meet minimums (xs: 32px+, others: 36px+)
  • Focus Indicators: Visible outline on focus
  • Keyboard Navigation: Full keyboard support via Radix UI
  • Disabled State: Opacity and pointer-events for clear indication

Best Practices

// ✓ Always provide value prop for controlled state
<RadioCardsRoot value={selected} onValueChange={setSelected}>
  <RadioCardsItem value="option-1">
    <RadioCardsTitle>Option 1</RadioCardsTitle>
  </RadioCardsItem>
</RadioCardsRoot>

// ✓ Use unique values for each item
<RadioCardsItem value="unique-1">...</RadioCardsItem>
<RadioCardsItem value="unique-2">...</RadioCardsItem>

// ✓ Provide meaningful titles and labels
<RadioCardsItem value="pro">
  <RadioCardsLabel>Professional Plan</RadioCardsLabel>
  <RadioCardsTitle>$29/month</RadioCardsTitle>
</RadioCardsItem>

Common Patterns

Pricing Selection

import { useState } from 'react';

function PricingSelector() {
  const [plan, setPlan] = useState('basic');

  return (
    <RadioCardsRoot value={plan} onValueChange={setPlan} size="lg">
      <RadioCardsItem value="basic">
        <RadioCardsLabel>Basic</RadioCardsLabel>
        <RadioCardsTitle>$9/mo</RadioCardsTitle>
      </RadioCardsItem>
      <RadioCardsItem value="pro">
        <RadioCardsLabel>Pro</RadioCardsLabel>
        <RadioCardsTitle>$29/mo</RadioCardsTitle>
      </RadioCardsItem>
      <RadioCardsItem value="enterprise">
        <RadioCardsLabel>Enterprise</RadioCardsLabel>
        <RadioCardsTitle>Custom</RadioCardsTitle>
      </RadioCardsItem>
    </RadioCardsRoot>
  );
}

Donation Amount

<RadioCardsRoot value={amount} onValueChange={setAmount} className="flex gap-[var(--scale-8)]">
  {[25, 50, 100, 250].map((value) => (
    <RadioCardsItem key={value} value={String(value)} className="flex-1">
      <RadioCardsTitle>${value}</RadioCardsTitle>
    </RadioCardsItem>
  ))}
</RadioCardsRoot>

Shipping Options

<RadioCardsRoot value={shipping} onValueChange={setShipping} size="md">
  <RadioCardsItem value="standard">
    <RadioCardsTitle>Standard</RadioCardsTitle>
    <RadioCardsLabel>5-7 business days</RadioCardsLabel>
  </RadioCardsItem>
  <RadioCardsItem value="express">
    <RadioCardsTitle>Express</RadioCardsTitle>
    <RadioCardsLabel>2-3 business days</RadioCardsLabel>
  </RadioCardsItem>
  <RadioCardsItem value="overnight">
    <RadioCardsTitle>Overnight</RadioCardsTitle>
    <RadioCardsLabel>Next day delivery</RadioCardsLabel>
  </RadioCardsItem>
</RadioCardsRoot>

Form Validation

<form onSubmit={handleSubmit}>
  <RadioCardsRoot required danger={hasError} value={selection} onValueChange={setSelection}>
    <RadioCardsItem value="option-1">
      <RadioCardsTitle>Option 1</RadioCardsTitle>
    </RadioCardsItem>
    <RadioCardsItem value="option-2">
      <RadioCardsTitle>Option 2</RadioCardsTitle>
    </RadioCardsItem>
  </RadioCardsRoot>
  {hasError && <span className="text-sm text-red-600">Please select an option</span>}
</form>

Migration Guide

Migrating to RadioGroup

Replace this deprecated component with RadioGroup and custom card styling for better maintainability:

Before (RadioCards - Deprecated)

import { RadioCardsRoot, RadioCardsItem, RadioCardsTitle, RadioCardsLabel } from '@pmi/catalyst-radio-cards';

<RadioCardsRoot value={value} onValueChange={setValue}>
  <RadioCardsItem value="option-1">
    <RadioCardsLabel>Plan Name</RadioCardsLabel>
    <RadioCardsTitle>$100</RadioCardsTitle>
  </RadioCardsItem>
  <RadioCardsItem value="option-2">
    <RadioCardsLabel>Plan Name</RadioCardsLabel>
    <RadioCardsTitle>$200</RadioCardsTitle>
  </RadioCardsItem>
</RadioCardsRoot>;
import { RadioGroup } from '@pmi/catalyst-radio-group';

<RadioGroup value={value} onValueChange={setValue} className="flex gap-3">
  <RadioGroup.Item value="option-1" className="flex flex-col p-4 border-2 rounded-lg hover:bg-gray-50">
    <RadioGroup.Indicator className="sr-only" />
    <span className="text-sm text-gray-600">Plan Name</span>
    <span className="text-2xl font-bold">$100</span>
  </RadioGroup.Item>
  <RadioGroup.Item value="option-2" className="flex flex-col p-4 border-2 rounded-lg hover:bg-gray-50">
    <RadioGroup.Indicator className="sr-only" />
    <span className="text-sm text-gray-600">Plan Name</span>
    <span className="text-2xl font-bold">$200</span>
  </RadioGroup.Item>
</RadioGroup>;

Troubleshooting

Cards Not Changing State

Ensure value and onValueChange are properly connected:

// ✓ Correct - controlled state
const [value, setValue] = useState('')
<RadioCardsRoot value={value} onValueChange={setValue}>
  ...
</RadioCardsRoot>

// ✗ Incorrect - missing onValueChange
<RadioCardsRoot value={value}>
  ...
</RadioCardsRoot>

Layout Shift on Selection

This is expected behavior due to border width change. The padding compensation may not be perfect for all custom styles.

// Normal: 2px border, full padding
// Selected: 4px border, reduced padding
// Net result: Card size stays same (mostly)

Hover Colors Not Showing

Ensure you're not overriding the group-hover/item: classes:

// ✓ Correct - preserves hover behavior
<RadioCardsItem className="custom-class">
  ...
</RadioCardsItem>

// ⚠ Warning - may override hover colors
<RadioCardsTitle className="text-blue-500">
  ...
</RadioCardsTitle>

Deprecation Notice

Component Deprecated

This component is deprecated and will be removed in a future major version.

Migration Path:

  • Use RadioGroup with custom card-style CSS
  • Simpler component structure (fewer nested components)
  • More flexible styling options
  • Better accessibility with automatic ARIA
  • Easier to maintain and customize

Timeline:

  • Current: Deprecated but functional
  • Support: Maintained for bug fixes only
  • Removal: Planned for next major version

Please migrate to RadioGroup with custom styling at your earliest convenience.

  • RadioGroup - Recommended replacement with custom styling
  • Radio - Deprecated basic radio input
  • Checkbox - For multiple selections
  • Card - For general card layouts

External Resources

On this page