React shipping method selector
Delivery options as selectable cards in React: a native radio group, one tab stop, the chosen card styled with :has(:checked).
Install
One command copies the files into your project, as code you then own. It needs a
components.json, and the shadcn registry page has the one-time setup.Terminal
npx shadcn@latest add https://www.box-kite.dev/r/shipping-method.jsonLight and dark
The same component twice, each half in a theme of its own — nothing in it is written for either. Open it full screen to see it in yours.
light
Delivery
dark
Delivery
Source
The files the command writes, exactly as the preview above runs them. Every style is a prop, so there is no stylesheet to copy with them.
box-kite/store/shipping-method.tsx
JSX
'use client';
import Flex from '@box-kite/react/components/flex';
import RadioGroup from '@box-kite/react/components/radioGroup';
import { Span } from '@box-kite/react/components/semantics';
import { formatPrice, type ShippingMethod as Method, shippingMethods } from './store-data';
export interface ShippingMethodProps {
methods?: readonly Method[];
value?: string;
onValueChange?: (id: string) => void;
/** Standard shipping is free above this subtotal. */
freeStandard?: boolean;
}
/**
* Delivery options as cards. Each card is the `<label>` of a native radio, so the whole card is the
* target, the set is one tab stop with the arrow keys inside it, and the chosen card is styled with
* `:has(:checked)` — the radio itself stays visible, because it is what says this is a choice of one.
*/
export default function ShippingMethod({ methods = shippingMethods, value, onValueChange, freeStandard }: ShippingMethodProps) {
const priceOf = (method: Method) => (freeStandard && method.id === 'standard' ? 0 : method.price);
return (
<RadioGroup
label="Delivery"
name="shipping.method"
value={value}
defaultValue={value === undefined ? methods[0].id : undefined}
onValueChange={(id) => id && onValueChange?.(id)}
gap={3}
>
{methods.map((method) => (
<RadioGroup.Item
key={method.id}
value={method.id}
flexShrink={0}
labelProps={{
display: 'flex',
ai: 'center',
gap: 3,
p: 4,
b: 1,
borderRadius: 3,
cursor: 'pointer',
borderColor: 'stone-300',
bgColor: 'transparent',
hover: { borderColor: 'stone-500' },
has: { ':checked': { borderColor: 'stone-900', ring: 1, ringColor: 'stone-900' } },
theme: {
dark: {
borderColor: 'stone-700',
bgColor: 'transparent',
hover: { borderColor: 'stone-500' },
has: { ':checked': { borderColor: 'stone-100', ringColor: 'stone-100' } },
},
},
}}
label={
<Flex flex1 jc="space-between" ai="center" gap={4}>
<Flex d="column" gap={0.5}>
<Span fontWeight={500}>{method.name}</Span>
<Span fontSize={14} color="stone-600" theme={{ dark: { color: 'stone-400' } }}>
{method.detail}
</Span>
</Flex>
<Span fontWeight={500}>{priceOf(method) ? formatPrice(priceOf(method)) : 'Free'}</Span>
</Flex>
}
/>
))}
</RadioGroup>
);
}
It stands on
store-design, which the command installs with it. The design the store blocks and pages share: the components restyled with Box.createComponents(), the catalogue, the drawn product art, and the header and footer. The design is on the template page.