Radio
Single-select radio input.
Installation
$
npx @309-thingspire/ui@latest add radioUsage
import { Radio } from "@/components/radio/radio"<Radio />Examples
Live preview rendered from Radio.preview.tsx. Switch to the Code tab to view the underlying component source.
Loading preview…
import React, { useState } from 'react';
import { border, colors, radius, shadows, spacing } from '../../style-tokens';
import type { RadioProps, RadioSize, RadioVisualState } from './Radio.types';
const palette = colors.primitive.palette;
type SizeConfig = {
controlSize: number;
dotSize: number;
};
const SIZE_CONFIG: Record<RadioSize, SizeConfig> = {
sm: {
controlSize: spacing.scale['16'],
dotSize: spacing.scale['8'],
},
md: {
controlSize: spacing.scale['20'],
dotSize: spacing.scale['10'],
},
};
function resolveVisualState(
forcedState: RadioVisualState | undefined,
disabled: boolean,
hovered: boolean,
focused: boolean,
): RadioVisualState {
if (disabled || forcedState === 'disabled') {
return 'disabled';
}
if (forcedState && forcedState !== 'default') {
return forcedState;
}
if (focused) {
return 'focus';
}
if (hovered) {
return 'hover';
}
return 'default';
}
export function Radio({
id,
name,
value,
className,
style,
size = 'sm',
checked = false,
state,
disabled = false,
onCheckedChange,
onClick,
ariaLabel,
}: RadioProps) {
const [hovered, setHovered] = useState(false);
const [focused, setFocused] = useState(false);
const config = SIZE_CONFIG[size];
const visualState = resolveVisualState(state, disabled, hovered, focused);
const isDisabled = visualState === 'disabled';
const backgroundColor = (() => {
if (isDisabled && !checked) {
return palette.gray['3'];
}
return palette.base.white;
})();
const borderWidth = (() => {
if (isDisabled && !checked) {
return border.width['0'];
}
if (checked) {
return border.width['2'];
}
return border.width['1'];
})();
const borderColor = (() => {
if (isDisabled && checked) {
return palette.gray['2'];
}
if (checked) {
return palette.purple['8'];
}
if (visualState === 'hover') {
return palette.gray['4'];
}
return palette.gray['3'];
})();
const boxShadow = (() => {
if (isDisabled) {
return 'none';
}
if (visualState === 'focus') {
return shadows.focusRing.light.css;
}
return shadows.elevation.xs.css;
})();
const handleSelect = () => {
if (isDisabled) {
return;
}
if (!checked) {
onCheckedChange?.(true);
}
onClick?.();
};
return (
<button
id={id}
name={name}
value={value}
type="button"
role="radio"
aria-checked={checked}
aria-label={ariaLabel}
disabled={isDisabled}
onClick={handleSelect}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
className={className}
style={{
width: config.controlSize,
height: config.controlSize,
display: 'grid',
placeItems: 'center',
padding: spacing.scale['0'],
margin: spacing.scale['0'],
borderStyle: 'solid',
borderWidth,
borderColor,
borderRadius: radius.scale.full,
backgroundColor,
boxShadow,
cursor: isDisabled ? 'not-allowed' : 'pointer',
boxSizing: 'border-box',
...style,
}}
>
{checked ? (
<span
aria-hidden="true"
style={{
width: config.dotSize,
height: config.dotSize,
borderRadius: radius.scale.full,
backgroundColor: isDisabled ? palette.gray['3'] : palette.purple['8'],
display: 'block',
}}
/>
) : null}
</button>
);
}
API Reference
Props 문서 준비 중입니다. (component-spec.json 추가 시 표시됩니다.)