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.
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.
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.' },
]}
/>
);
}Voice input
Talk to Flo instead of typing. The mic dictates into the box to check before sending; holding ⌘⇧Space talks and letting go sends. Words stream into the box as they are heard.
Command palette
The ⌘K box. Search every record, run an action, go to a page or ask Flo, from anywhere. It grows out of the Ask bar and folds back into it.