범용성을 가지는 체크박스를 구현한 내용을 공유하고자 한다.

import { HTMLProps, ReactNode, SVGProps, useId } from 'react';
import cns from 'classnames';
import * as S from './Checkbox.styles';
// v 아이콘
const CheckIcon = (props: SVGProps<SVGSVGElement>) => (
<svg width={10} height={8} fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M9.664.253a1 1 0 0 1 .083 1.411l-5.333 6a1 1 0 0 1-1.495 0l-2.666-3a1 1 0 1 1 1.494-1.328l1.92 2.159L8.253.335A1 1 0 0 1 9.664.254Z"
fill="currentColor"
/>
</svg>
);
interface CheckboxProps extends Omit<HTMLProps<HTMLInputElement>, 'label'> {
/** label 내용 */
label?: ReactNode;
/** check UI 위치 */
checkPosition?: 'left' | 'right';
/** checkbox 설명문 */
description?: ReactNode;
/** check UI 모양을 원형 활성화 */
isCircle?: boolean;
}
export function Checkbox({
//
label,
checkPosition = 'left',
isCircle,
description,
...rest
}: CheckboxProps) {
const checkBoxId = useId();
function _Checkbox() {
return (
<S.Checkbox
className={cns({
circle: isCircle,
})}
>
<input //
id={checkBoxId}
type="checkbox"
data-checkbox-control
{...rest}
/>
<span data-checkbox-icon>
<CheckIcon />
</span>
</S.Checkbox>
);
}
const isRight = checkPosition === 'right';
return (
<S.CheckboxWrapper>
{checkPosition === 'left' && <_Checkbox />}
<S.LabelWrapper>
{label && (
<label htmlFor={checkBoxId} className={`${isRight ? 'reverse' : ''}`}>
{label}
</label>
)}
{description && <p className={`${isRight ? 'reverse' : ''}`}>{description}</p>}
</S.LabelWrapper>
{checkPosition === 'right' && <_Checkbox />}
</S.CheckboxWrapper>
);
}
interface CheckboxProps extends Omit<HTMLProps<HTMLInputElement>, 'label'> {
/** label 내용 */
label?: ReactNode;
/** check UI 위치 */
checkPosition?: 'left' | 'right';
/** checkbox 설명문 */
description?: ReactNode;
/** check UI 모양을 원형 활성화 */
isCircle?: boolean;
}
const CheckIcon = (props: SVGProps<SVGSVGElement>) => (
<svg width={10} height={8} fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M9.664.253a1 1 0 0 1 .083 1.411l-5.333 6a1 1 0 0 1-1.495 0l-2.666-3a1 1 0 1 1 1.494-1.328l1.92 2.159L8.253.335A1 1 0 0 1 9.664.254Z"
fill="currentColor"
/>
</svg>
);
우선 기본적으로 제공하는 UI 를 사용하지 않기 때문에 check 를 표시하는 v UI 를 컴포넌트로 추출하였다.
function _Checkbox() {
return (
<S.Checkbox
className={cns({
circle: isCircle,
})}
>
<input //
id={checkBoxId}
type="checkbox"
data-checkbox-control
{...rest}
/>
<span data-checkbox-icon>
<CheckIcon />
</span>
</S.Checkbox>
);
}
export const Checkbox = styled.span`
position: relative;
z-index: 0;
display: inline-flex;
width: 24px;
height: 24px;
&.circle {
[data-checkbox-icon] {
border-radius: 9999px;
}
}
[data-checkbox-icon] {
position: absolute;
display: inline-flex;
align-items: center;
justify-content: center;
color: ${theme.color.gray300};
background-color: ${theme.color.white};
border: 2px solid ${theme.color.gray300};
border-radius: 2px;
inset: 2px;
}
[data-checkbox-control] {
// 기존 checkbox UI 숨기기
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
opacity: 0;
inset: 0;
&:checked + [data-checkbox-icon] {
color: ${theme.color.black};
background-color: ${theme.color.primary};
border-color: ${theme.color.primary};
}
&:disabled + [data-checkbox-icon] {
color: ${theme.color.gray200};
background-color: ${theme.color.gray0};
border-color: ${theme.color.gray200};
}
}
`;
data-checkbox-control 과 data-checkbox-icon 을 속성으로 정의하고 scss 에서 해당 값을 이용해서 스타일링 하였다.// ...
const isRight = checkPosition === 'right';
return (
<S.CheckboxWrapper>
{checkPosition === 'left' && <_Checkbox />}
<S.LabelWrapper>
{label && (
<label htmlFor={checkBoxId} className={`${isRight ? 'reverse' : ''}`}>
{label}
</label>
)}
{description && <p className={`${isRight ? 'reverse' : ''}`}>{description}</p>}
</S.LabelWrapper>
{checkPosition === 'right' && <_Checkbox />}
</S.CheckboxWrapper>
);