ComponentsSelection

Choice card

One choice from a short list, where each option needs a line to explain it. A radio group drawn as full-width cards; the whole card is the target.

Playground

Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.

States
import { ChoiceCards } from '@/ui/choice-card';<ChoiceCards />

In a settings section

Cards stack full width, 8px apart, in the field column. A choice saves on change and a toast confirms it. Pick Everything to see a failed save: the old choice comes back and an error toast stays until closed.

Visibility

What teammates see of mail you haven't linked to a job. Flo never reads it.

import { useState } from 'react';
import { ChoiceCards } from '@/ui/choice-card';
import { SettingsForm, SettingsSection } from '@/ui/field';
import { toast } from '@/ui/toast';

const visibility = [
  { value: 'everything', title: 'Everything', description: 'Subject, body and attachments.' },
  { value: 'subject', title: 'Subject and metadata', description: 'Subject, participants and time.' },
  { value: 'metadata', title: 'Metadata', description: 'Participants and time.' },
];

// A choice saves on change, like a switch; the cards wait while it saves, and a failed save puts the old choice back.
export default function MailVisibility() {
  const [value, setValue] = useState('metadata');
  const [saving, setSaving] = useState(false);
  return (
    <SettingsForm>
      <SettingsSection title="Visibility" description="What teammates see of mail you haven't linked to a job. Flo never reads it.">
        <ChoiceCards
          aria-label="Visibility"
          options={visibility}
          value={value}
          disabled={saving}
          onValueChange={(next) => {
            const before = value;
            setValue(next);
            setSaving(true);
            setTimeout(() => {
              setSaving(false);
              if (next === 'everything') {
                setValue(before);
                toast.add({ type: 'error', title: 'Couldn’t save mail sync', description: 'Check your connection and try again.' });
              } else toast.add({ type: 'success', title: 'Mail sync saved' });
            }, 900);
          }}
        />
      </SettingsSection>
    </SettingsForm>
  );
}

States

The selected card takes the selected fill and a 1px brand edge, and its radio fills brand green. Hover fills a card grey; focus draws the 2px brand ring around the whole card. A disabled option fades to 50%; the whole group does while a choice saves.

import { ChoiceCards } from '@/ui/choice-card';

const people = [
  { value: 'sent-received', title: 'Sent and received', description: 'People you’ve emailed, and people who emailed you.' },
  { value: 'sent', title: 'Sent', description: 'People you’ve sent emails to.' },
  { value: 'none', title: 'None', description: 'Nobody from your mail.', disabled: true },
];

// Rest, selected and a disabled option. Hover a card or Tab to the group to see hover and focus.
export default function States() {
  return <ChoiceCards aria-label="Add people from mail" options={people} defaultValue="sent" className="max-w-100" />;
}

Long text

Titles and lines wrap inside the card. The radio stays level with the first line.

import { ChoiceCards } from '@/ui/choice-card';

// Long titles and lines wrap inside the card; the radio stays on the first line.
export default function LongText() {
  return (
    <ChoiceCards
      aria-label="CV template"
      className="max-w-90"
      defaultValue="anonymised"
      options={[
        { value: 'branded', title: 'Northbridge Talent branded template with the consultant’s contact block', description: 'Your logo, colours and the consultant’s name, phone and email on every page of the CV.' },
        { value: 'anonymised', title: 'Anonymised', description: 'Name, photo, contact details and current employer removed, as clients ask before a first call.' },
      ]}
    />
  );
}

On this page