Checkbox
Two- or three-state checkable input.
Installation
$
npx @309-thingspire/ui@latest add checkboxUsage
import { Checkbox } from "@/components/checkbox/checkbox"<Checkbox />Examples
Live preview rendered from Checkbox.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 { CheckboxProps, CheckboxSize, CheckboxType, CheckboxVisualState } from './Checkbox.types';
/**
* Check / minus glyphs are inlined from the Figma source
* (carbonscope-Library v1.0, nodes 1405:51283 / 1405:51284) rather than
* pulled from IconLibrary. The Figma vectors are stroke-based with very
* specific viewBox / stroke-width tuned for the 16/20/24px checkbox box
* — using IconLibrary's fill-based variants would change the line weight.
*/
function CheckGlyph() {
return (
<svg
preserveAspectRatio="none"
width="100%"
height="100%"
overflow="visible"
viewBox="0 0 9.16211 7.23481"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
style={{ display: 'block' }}
>
<path
d="M0.58987 2.77053L3.50654 6.48481L8.58987 0.484813"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
);
}
function MinusGlyph() {
return (
<svg
preserveAspectRatio="none"
width="100%"
height="100%"
overflow="visible"
viewBox="0 0 8 2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
style={{ display: 'block' }}
>
<path d="M0 1H8" stroke="currentColor" strokeWidth="2" />
</svg>
);
}
const palette = colors.primitive.palette;
const textBase = colors.semantic.theme.text.base;
type SizeConfig = {
boxSize: number;
iconCheckHeight: number;
iconMinusHeight: number;
};
const SIZE_CONFIG: Record<CheckboxSize, SizeConfig> = {
sm: {
boxSize: spacing.scale['16'],
iconCheckHeight: spacing.scale['6'],
iconMinusHeight: spacing.scale['2'],
},
md: {
boxSize: spacing.scale['20'],
iconCheckHeight: spacing.scale['6'],
iconMinusHeight: spacing.scale['2'],
},
};
function resolveVisualState(
forcedState: CheckboxVisualState | undefined,
disabled: boolean,
hovered: boolean,
focused: boolean,
): CheckboxVisualState {
if (disabled || forcedState === 'disabled') {
return 'disabled';
}
if (forcedState && forcedState !== 'default') {
return forcedState;
}
if (focused) {
return 'focus';
}
if (hovered) {
return 'hover';
}
return 'default';
}
function resolveChecked(type: CheckboxType, checked: boolean): boolean {
if (type === 'indeterminate') {
return false;
}
return checked;
}
function resolveIcon(type: CheckboxType, checked: boolean): 'check' | 'minus' | null {
if (type === 'indeterminate') {
return 'minus';
}
return checked ? 'check' : null;
}
export function Checkbox({
id,
name,
value,
className,
style,
size = 'sm',
type = 'default',
checked = false,
state,
disabled = false,
onCheckedChange,
onClick,
ariaLabel,
}: CheckboxProps) {
const [hovered, setHovered] = useState(false);
const [focused, setFocused] = useState(false);
const controlledChecked = resolveChecked(type, checked);
const visualState = resolveVisualState(state, disabled, hovered, focused);
const isDisabled = visualState === 'disabled';
const icon = resolveIcon(type, controlledChecked);
const config = SIZE_CONFIG[size];
const backgroundColor = (() => {
if (isDisabled) {
return palette.gray['3'];
}
if (icon) {
return palette.purple['8'];
}
return palette.base.white;
})();
const borderColor = (() => {
if (isDisabled) {
return palette.base.transparent;
}
if (icon) {
return palette.base.transparent;
}
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 handleToggle = () => {
if (isDisabled) {
return;
}
if (type === 'indeterminate') {
onCheckedChange?.(false);
onClick?.();
return;
}
onCheckedChange?.(!controlledChecked);
onClick?.();
};
return (
<button
id={id}
name={name}
value={value}
type="button"
role="checkbox"
aria-checked={type === 'indeterminate' ? 'mixed' : controlledChecked}
aria-label={ariaLabel}
disabled={isDisabled}
onClick={handleToggle}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
className={className}
style={{
width: config.boxSize,
height: config.boxSize,
display: 'grid',
placeItems: 'center',
padding: spacing.scale['0'],
margin: spacing.scale['0'],
borderStyle: 'solid',
borderWidth: icon || isDisabled ? border.width['0'] : border.width['1'],
borderColor,
borderRadius: radius.scale.xs,
backgroundColor,
boxShadow,
cursor: isDisabled ? 'not-allowed' : 'pointer',
boxSizing: 'border-box',
...style,
}}
>
{icon ? (
<span
aria-hidden="true"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: spacing.scale['8'],
height: icon === 'check' ? config.iconCheckHeight : config.iconMinusHeight,
color: textBase.staticWhite,
overflow: 'visible',
}}
>
{icon === 'check' ? <CheckGlyph /> : <MinusGlyph />}
</span>
) : null}
</button>
);
}
API Reference
Props 문서 준비 중입니다. (component-spec.json 추가 시 표시됩니다.)