All components
Radio Card
Single-choice cards with label, description, and radio indicator in surface, subtle, outline, and solid variants.
1234567891011121314151617181920212223import { RadioCard } from "#components/ui/ark-ui/radio-card.tsx";import { indigoPalette } from "@stylexblocks/ui/themes/palettes.ts";
export function Example() { return ( // Root is an Ark radio group styled as cards; it mirrors the selected // value so each item can style itself from its checked state <RadioCard.Root defaultValue="pro" variant="outline" size="md" palette={indigoPalette} aria-label="Plan"> <RadioCard.Item value="pro"> <RadioCard.ItemControl> <RadioCard.ItemContent> <RadioCard.ItemText>Pro</RadioCard.ItemText> <RadioCard.ItemDescription>For production apps</RadioCard.ItemDescription> </RadioCard.ItemContent> <RadioCard.ItemIndicator /> </RadioCard.ItemControl> <RadioCard.ItemHiddenInput /> </RadioCard.Item> </RadioCard.Root> );}
// controlled: <RadioCard.Root value={plan} onValueChange={(d) => setPlan(d.value)}>Variants
surface
subtle
outline
solid
Sizes
sm
md
lg
Install
1npx stylexblocks add radio-cardSource
components/ui/ark-ui/radio-card.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605import * as stylex from "@stylexjs/stylex";import { RadioGroup as ArkRadioGroup, useRadioGroupContext, useRadioGroupItemContext } from "@ark-ui/react/radio-group";import { createSlotRecipeContext } from "#lib/ark-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, fromMap, resolveSlotStyles, type SlotRecipeVariants,} from "#lib/recipe-props.ts";import { radioCardItemIndicatorDotVars } from '@stylexblocks/ui/slot-recipes/radio-card.stylex.ts'
import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { colors } from '@stylexblocks/ui/tokens/colors.stylex.ts'import { cursor } from '@stylexblocks/ui/tokens/cursor.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { radii } from '@stylexblocks/ui/tokens/radii.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'
const radioCardRoot = stylex.create({ base: { display: 'flex', flexDirection: 'column', gap: spacing._1_5, isolation: 'isolate', },})
const radioCardItem = stylex.create({ base: { flex: 1, display: 'flex', flexDirection: 'column', userSelect: 'none', position: 'relative', borderRadius: semanticRadii.panel, backgroundColor: { default: null, ':focus': `color-mix(in oklch, ${colorPalette.muted} 20%, transparent)`, }, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.8, }, borderColor: { default: null, ':is(:disabled, [disabled], [data-disabled])': semanticColors.borderSubtle, }, zIndex: { default: null, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': 1, }, },})
const radioCardItemSizes = stylex.create({ sm: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, md: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, lg: { fontSize: fontSizes.md, lineHeight: lineHeights.md, },})
const radioCardItemVariants = stylex.create({ surface: { borderWidth: '1px', backgroundColor: { default: null, ':focus': `color-mix(in oklch, ${colorPalette.muted} 20%, transparent)`, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.subtle, }, color: { default: null, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.fg, }, borderColor: { default: null, ':is(:disabled, [disabled], [data-disabled])': semanticColors.borderSubtle, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.muted, }, }, subtle: { backgroundColor: { default: semanticColors.bgMuted, ':focus': `color-mix(in oklch, ${colorPalette.muted} 20%, transparent)`, }, }, outline: { borderWidth: '1px', boxShadow: { default: null, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': '0 0 0 1px var(--shadow-color)', }, '--shadow-color': { default: null, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.solid, }, borderColor: { default: null, ':is(:disabled, [disabled], [data-disabled])': semanticColors.borderSubtle, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.solid, }, }, solid: { borderWidth: '1px', backgroundColor: { default: null, ':focus': `color-mix(in oklch, ${colorPalette.muted} 20%, transparent)`, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.solid, }, color: { default: null, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.contrast, }, borderColor: { default: null, ':is(:disabled, [disabled], [data-disabled])': semanticColors.borderSubtle, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.solid, }, },})
const radioCardItemJustifys = stylex.create({ start: { '--radio-card-justify': 'flex-start', }, end: { '--radio-card-justify': 'flex-end', }, center: { '--radio-card-justify': 'center', },})
const radioCardItemAligns = stylex.create({ start: { '--radio-card-align': 'flex-start', }, end: { '--radio-card-align': 'flex-end', }, center: { '--radio-card-align': 'center', },})
const radioCardLabel = stylex.create({ base: { display: 'inline-flex', fontWeight: fontWeights.medium, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, },})
const radioCardItemText = stylex.create({ base: { fontWeight: fontWeights.medium, },})
const radioCardItemDescription = stylex.create({ base: { opacity: 0.64, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, },})
const radioCardItemControl = stylex.create({ base: { display: 'inline-flex', flex: 1, position: 'relative', borderRadius: 'inherit', justifyContent: 'var(--radio-card-justify)', alignItems: 'var(--radio-card-align)', backgroundColor: { default: null, ':is(:disabled, [disabled], [data-disabled])': semanticColors.bgMuted, }, },})
const radioCardItemControlSizes = stylex.create({ sm: { padding: spacing._3, gap: spacing._1_5, }, md: { padding: spacing._4, gap: spacing._2_5, }, lg: { padding: spacing._4, gap: spacing._3_5, },})
const radioCardItemControlVariants = stylex.create({ subtle: { backgroundColor: { default: null, ':is(:disabled, [disabled], [data-disabled])': semanticColors.bgMuted, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.muted, }, color: { default: null, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.fg, }, },})
const radioCardItemControlAligns = stylex.create({ start: { textAlign: 'start', }, end: { textAlign: 'end', }, center: { textAlign: 'center', },})
const radioCardItemControlOrientations = stylex.create({ vertical: { flexDirection: 'column', }, horizontal: { flexDirection: 'row', },})
const radioCardItemIndicator = stylex.create({ base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, verticalAlign: 'top', color: colors.white, borderWidth: '1px', borderColor: { default: 'transparent', ':is([data-invalid], [aria-invalid="true"])': semanticColors.borderDestructive, }, borderRadius: radii.full, cursor: { default: cursor.radio, ':is(:disabled, [disabled], [data-disabled])': cursor.disabled, }, outlineWidth: { default: null, ':focus-visible': '1px', }, outlineOffset: { default: null, ':focus-visible': '2px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, [radioCardItemIndicatorDotVars.height]: '100%', [radioCardItemIndicatorDotVars.width]: '100%', [radioCardItemIndicatorDotVars.borderRadius]: radii.full, [radioCardItemIndicatorDotVars.backgroundColor]: 'currentColor', [radioCardItemIndicatorDotVars.scale]: 0.4, },})
const radioCardItemIndicatorSizes = stylex.create({ sm: { width: sizes._3_5, height: sizes._3_5, }, md: { width: sizes._4, height: sizes._4, }, lg: { width: sizes._5, height: sizes._5, },})
const radioCardItemIndicatorVariants = stylex.create({ surface: { borderWidth: '1px', borderColor: { default: semanticColors.border, ':is([data-invalid], [aria-invalid="true"])': semanticColors.borderDestructive, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.solid, }, backgroundColor: { default: null, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.solid, }, color: { default: colors.white, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.contrast, }, }, subtle: { borderWidth: '1px', borderColor: { default: 'inherit', ':is([data-invalid], [aria-invalid="true"])': semanticColors.borderDestructive, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.solid, }, color: { default: colors.white, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.fg, }, [radioCardItemIndicatorDotVars.scale]: 0.6, }, outline: { borderWidth: '1px', borderColor: { default: semanticColors.border, ':is([data-invalid], [aria-invalid="true"])': semanticColors.borderDestructive, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.solid, }, backgroundColor: { default: null, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.solid, }, color: { default: colors.white, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.contrast, }, }, solid: { backgroundColor: semanticColors.bg, borderWidth: '1px', borderColor: { default: 'inherit', ':is([data-invalid], [aria-invalid="true"])': semanticColors.borderDestructive, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': 'currentcolor', }, color: { default: colors.white, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': colorPalette.solid, }, },})
const radioCardItemIndicatorDot = stylex.create({ base: { height: radioCardItemIndicatorDotVars.height, width: radioCardItemIndicatorDotVars.width, borderRadius: radioCardItemIndicatorDotVars.borderRadius, backgroundColor: radioCardItemIndicatorDotVars.backgroundColor, scale: radioCardItemIndicatorDotVars.scale, },})
const radioCardItemAddon = stylex.create({ base: { roundedBottom: 'inherit', color: { default: null, ':is(:disabled, [disabled], [data-disabled])': semanticColors.fgMuted, }, },})
const radioCardItemAddonSizes = stylex.create({ sm: { paddingInline: spacing._3, paddingBlock: spacing._1_5, borderTopWidth: '1px', }, md: { paddingInline: spacing._4, paddingBlock: spacing._2, borderTopWidth: '1px', }, lg: { paddingInline: spacing._4, paddingBlock: spacing._2, borderTopWidth: '1px', },})
const radioCardItemContent = stylex.create({ base: { display: 'flex', flexDirection: 'column', flex: 1, gap: spacing._1, justifyContent: 'var(--radio-card-justify)', alignItems: 'var(--radio-card-align)', },})
export type RadioCardSize = keyof typeof radioCardItemSizes
export type RadioCardVariant = keyof typeof radioCardItemVariants
export type RadioCardJustify = keyof typeof radioCardItemJustifys
export type RadioCardAlign = keyof typeof radioCardItemAligns
export type RadioCardOrientation = keyof typeof radioCardItemControlOrientations
const radioCardRecipe = defineSlotRecipe({ slots: { root: { styles: radioCardRoot }, item: { styles: radioCardItem, sizes: radioCardItemSizes, variants: radioCardItemVariants, justifys: radioCardItemJustifys, aligns: radioCardItemAligns, }, itemText: { styles: radioCardItemText }, itemDescription: { styles: radioCardItemDescription }, itemControl: { styles: radioCardItemControl, sizes: radioCardItemControlSizes, variants: radioCardItemControlVariants, aligns: radioCardItemControlAligns, orientations: radioCardItemControlOrientations, }, itemIndicator: { styles: radioCardItemIndicator, sizes: radioCardItemIndicatorSizes, variants: radioCardItemIndicatorVariants, }, itemContent: { styles: radioCardItemContent }, }, defaultVariants: { size: "md", variant: "outline", align: "start", justify: "start", orientation: "horizontal", },});
const cardChecked = stylex.create({ surface: { backgroundColor: { default: null, ":is([data-checked])": colorPalette.subtle }, color: { default: null, ":is([data-checked])": colorPalette.fg }, borderColor: { default: null, ":is([data-checked])": colorPalette.muted }, }, subtle: { backgroundColor: { default: null, ":is([data-checked])": colorPalette.muted }, color: { default: null, ":is([data-checked])": colorPalette.fg }, }, outline: { boxShadow: { default: null, ":is([data-checked])": `0 0 0 1px ${colorPalette.solid}` }, borderColor: { default: null, ":is([data-checked])": colorPalette.solid }, }, solid: { backgroundColor: { default: null, ":is([data-checked])": colorPalette.solid }, color: { default: null, ":is([data-checked])": colorPalette.contrast }, borderColor: { default: null, ":is([data-checked])": colorPalette.solid }, },});
const cardIndicatorChecked = stylex.create({ surface: { backgroundColor: { default: null, ":is([data-checked])": colorPalette.solid }, color: { default: null, ":is([data-checked])": colorPalette.contrast }, borderColor: { default: null, ":is([data-checked])": colorPalette.solid }, }, subtle: { color: { default: null, ":is([data-checked])": colorPalette.fg }, borderColor: { default: null, ":is([data-checked])": colorPalette.solid }, }, outline: { backgroundColor: { default: null, ":is([data-checked])": colorPalette.solid }, color: { default: null, ":is([data-checked])": colorPalette.contrast }, borderColor: { default: null, ":is([data-checked])": colorPalette.solid }, }, solid: { color: { default: null, ":is([data-checked])": colorPalette.solid }, borderColor: { default: null, ":is([data-checked])": "currentcolor" }, },});
const chrome = stylex.create({ cursorPointer: { cursor: "pointer" },});
function ItemNode({ value, ...props }: React.ComponentProps<typeof ArkRadioGroup.Item>) { const group = useRadioGroupContext(); const checked = group.value === value; return <ArkRadioGroup.Item {...props} value={value} data-checked={checked ? "" : undefined} />;}
function ItemIndicatorNode({ children, ...props}: React.ComponentProps<typeof ArkRadioGroup.ItemControl>) { const item = useRadioGroupItemContext(); return ( <ArkRadioGroup.ItemControl {...props} data-checked={item.checked ? "" : undefined}> {children ?? (item.checked ? <span {...stylex.props(radioCardItemIndicatorDot.base)} /> : null)} </ArkRadioGroup.ItemControl> );}
const { withProvider, withContext } = createSlotRecipeContext(radioCardRecipe);
export type RadioCardVariantProps = Partial<SlotRecipeVariants<typeof radioCardRecipe>>;
/** * Radio Card — Ark UI's radio-group machine styled as selectable cards. * `<RadioCard.Root variant size>` resolves the recipe once; every part * inherits it through context. Item and ItemIndicator copy `data-checked` * from the machine so checked chrome can target `:is([data-checked])`. */export const RadioCard = { Root: withProvider(ArkRadioGroup.Root, "root", "RadioCardRoot"), Item: withContext( ItemNode, (v) => [ ...resolveSlotStyles(radioCardRecipe.slots.item, v), fromMap(cardChecked, v.variant), chrome.cursorPointer, ], "RadioCardItem", ), ItemControl: withContext("span", "itemControl", "RadioCardItemControl"), ItemContent: withContext("span", "itemContent", "RadioCardItemContent"), ItemText: withContext(ArkRadioGroup.ItemText, "itemText", "RadioCardItemText"), ItemDescription: withContext("span", "itemDescription", "RadioCardItemDescription"), ItemIndicator: withContext( ItemIndicatorNode, (v) => [ ...resolveSlotStyles(radioCardRecipe.slots.itemIndicator, v), fromMap(cardIndicatorChecked, v.variant), ], "RadioCardItemIndicator", ), ItemHiddenInput: ArkRadioGroup.ItemHiddenInput,};
export type RadioCardRootProps = React.ComponentProps<typeof RadioCard.Root>;export type RadioCardItemProps = React.ComponentProps<typeof RadioCard.Item>;export type RadioCardItemControlProps = React.ComponentProps<typeof RadioCard.ItemControl>;export type RadioCardItemContentProps = React.ComponentProps<typeof RadioCard.ItemContent>;export type RadioCardItemTextProps = React.ComponentProps<typeof RadioCard.ItemText>;export type RadioCardItemDescriptionProps = React.ComponentProps<typeof RadioCard.ItemDescription>;export type RadioCardItemIndicatorProps = React.ComponentProps<typeof RadioCard.ItemIndicator>;