React
Checkbox
사용자가 하나 이상의 옵션을 선택할 수 있게 해주는 컴포넌트입니다. 목록에서 여러 항목을 선택하거나 약관 동의와 같은 선택적 작업에 사용됩니다.
import { VStack } from "@seed-design/react";
import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox";
export default function CheckboxPreview() {
return (
<VStack p="x6">
<CheckboxGroup
label="관심 분야"
description="관심 있는 분야를 모두 선택해 주세요."
indicator="선택"
>
<Checkbox label="디자인" tone="neutral" size="large" />
<Checkbox label="개발" tone="neutral" size="large" defaultChecked />
<Checkbox label="마케팅" tone="neutral" size="large" />
</CheckboxGroup>
</VStack>
);
}Installation
npx @seed-design/cli@latest add ui:checkboxpnpm dlx @seed-design/cli@latest add ui:checkboxyarn dlx @seed-design/cli@latest add ui:checkboxbun x @seed-design/cli@latest add ui:checkbox의존성 설치
npm install @karrotmarket/react-monochrome-icon @seed-design/css @seed-design/reactyarn add @karrotmarket/react-monochrome-icon @seed-design/css @seed-design/reactpnpm add @karrotmarket/react-monochrome-icon @seed-design/css @seed-design/reactbun add @karrotmarket/react-monochrome-icon @seed-design/css @seed-design/react아래 코드를 복사 후 붙여넣고 사용하세요
/**
* @file ui:checkbox
* @requires @seed-design/react@^2.0.0
* @requires @seed-design/css@^2.0.0
**/
"use client";
import IconCheckmarkFatFill from "@karrotmarket/react-monochrome-icon/IconCheckmarkFatFill";
import IconMinusFatFill from "@karrotmarket/react-monochrome-icon/IconMinusFatFill";
import IconExclamationmarkCircleFill from "@karrotmarket/react-monochrome-icon/IconExclamationmarkCircleFill";
import {
Checkbox as SeedCheckbox,
Fieldset as SeedFieldset,
PrefixIcon,
VisuallyHidden,
} from "@seed-design/react";
import type { FieldLabelVariantProps } from "@seed-design/css/recipes/field-label";
import * as React from "react";
import {
checkboxGroup,
type CheckboxGroupVariantProps,
} from "@seed-design/css/recipes/checkbox-group";
export interface CheckboxGroupProps extends SeedFieldset.RootProps, CheckboxGroupVariantProps {
label?: React.ReactNode;
/**
* @default "medium"
*/
labelWeight?: FieldLabelVariantProps["weight"];
indicator?: React.ReactNode;
showRequiredIndicator?: boolean;
description?: React.ReactNode;
errorMessage?: React.ReactNode;
}
/**
* @see https://seed-design.io/react/components/checkbox
*/
export const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
(
{
label,
labelWeight,
indicator,
showRequiredIndicator,
description,
errorMessage,
children,
...props
},
ref,
) => {
const [variantProps, restProps] = checkboxGroup.splitVariantProps(props);
if (
process.env.NODE_ENV !== "production" &&
!label &&
!restProps["aria-label"] &&
!restProps["aria-labelledby"]
) {
console.warn(
"CheckboxGroup component is recommended to have a `label`, `aria-label` or `aria-labelledby` attribute.",
);
}
return (
<SeedFieldset.Root ref={ref} {...restProps}>
{(label || indicator) && (
<SeedFieldset.Header>
<SeedFieldset.Label weight={labelWeight}>
{label}
{showRequiredIndicator && <SeedFieldset.RequiredIndicator />}
{indicator && <SeedFieldset.IndicatorText>{indicator}</SeedFieldset.IndicatorText>}
</SeedFieldset.Label>
</SeedFieldset.Header>
)}
<SeedCheckbox.Group {...variantProps}>{children}</SeedCheckbox.Group>
{(description || errorMessage) && (
<SeedFieldset.Footer>
{description &&
(errorMessage ? (
<VisuallyHidden asChild>
<SeedFieldset.Description>{description}</SeedFieldset.Description>
</VisuallyHidden>
) : (
<SeedFieldset.Description>{description}</SeedFieldset.Description>
))}
{errorMessage && (
<SeedFieldset.ErrorMessage>
<PrefixIcon svg={<IconExclamationmarkCircleFill />} />
{errorMessage}
</SeedFieldset.ErrorMessage>
)}
</SeedFieldset.Footer>
)}
</SeedFieldset.Root>
);
},
);
CheckboxGroup.displayName = "CheckboxGroup";
export interface CheckboxProps extends SeedCheckbox.RootProps {
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
rootRef?: React.Ref<HTMLLabelElement>;
label?: React.ReactNode;
}
/**
* @see https://seed-design.io/react/components/checkbox
*/
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
({ inputProps, rootRef, label, ...otherProps }, ref) => {
return (
<SeedCheckbox.Root ref={rootRef} {...otherProps}>
<SeedCheckbox.Control>
<SeedCheckbox.Indicator
unchecked={otherProps.variant === "ghost" ? <IconCheckmarkFatFill /> : null}
checked={<IconCheckmarkFatFill />}
indeterminate={<IconMinusFatFill />}
/>
</SeedCheckbox.Control>
<SeedCheckbox.Label>{label}</SeedCheckbox.Label>
<SeedCheckbox.HiddenInput ref={ref} {...inputProps} />
</SeedCheckbox.Root>
);
},
);
Checkbox.displayName = "Checkbox";
export interface CheckmarkProps extends SeedCheckbox.ControlProps {}
export const Checkmark = React.forwardRef<HTMLDivElement, CheckmarkProps>((props, ref) => {
return (
<SeedCheckbox.Control ref={ref} {...props}>
<SeedCheckbox.Indicator
unchecked={props.variant === "ghost" ? <IconCheckmarkFatFill /> : null}
checked={<IconCheckmarkFatFill />}
indeterminate={<IconMinusFatFill />}
/>
</SeedCheckbox.Control>
);
});
Checkmark.displayName = "Checkmark";
/**
* This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.
* You can extend this snippet however you want.
*/
Props
CheckboxGroup
Prop
Type
label?React.ReactNodeindicator?React.ReactNodeshowRequiredIndicator?boolean | undefineddescription?React.ReactNodeerrorMessage?React.ReactNodeCheckbox
Prop
Type
inputProps?React.InputHTMLAttributes<HTMLInputElement> | undefinedrootRef?React.Ref<HTMLLabelElement> | undefinedlabel?React.ReactNodedisabled?boolean | undefinedinvalid?boolean | undefinedrequired?boolean | undefinedchecked?boolean | undefineddefaultChecked?boolean | undefinedonCheckedChange?((checked: boolean) => void) | undefinedindeterminate?boolean | undefinedCheckmark
Prop
Type
Examples
Sizes
import { HStack } from "@seed-design/react";
import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox";
export default function CheckboxSize() {
return (
<HStack gap="x8" p="x6">
<CheckboxGroup aria-label="Square size examples">
<Checkbox label="Medium (default)" size="medium" defaultChecked tone="neutral" />
<Checkbox label="Large" size="large" defaultChecked tone="neutral" />
</CheckboxGroup>
<CheckboxGroup aria-label="Ghost size examples">
<Checkbox
label="Medium (default)"
size="medium"
variant="ghost"
defaultChecked
tone="neutral"
/>
<Checkbox label="Large" size="large" variant="ghost" defaultChecked tone="neutral" />
</CheckboxGroup>
</HStack>
);
}Tones and Variants
Brand
import { VStack } from "@seed-design/react";
import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox";
export default function CheckboxBrand() {
return (
<VStack p="x6">
<CheckboxGroup aria-label="Brand tone examples">
<Checkbox
label="Square (default)"
variant="square"
tone="brand"
size="large"
defaultChecked
/>
<Checkbox label="Ghost" variant="ghost" tone="brand" size="large" defaultChecked />
</CheckboxGroup>
</VStack>
);
}Neutral
import { VStack } from "@seed-design/react";
import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox";
export default function CheckboxNeutral() {
return (
<VStack p="x6">
<CheckboxGroup aria-label="Neutral tone examples">
<Checkbox
label="Square (default)"
variant="square"
tone="neutral"
size="large"
defaultChecked
/>
<Checkbox label="Ghost" variant="ghost" tone="neutral" size="large" defaultChecked />
</CheckboxGroup>
</VStack>
);
}Indeterminate
import { VStack } from "@seed-design/react";
import { Checkbox } from "seed-design/ui/checkbox";
export default function CheckboxIndeterminate() {
return (
<VStack p="x6">
<Checkbox defaultChecked label="indeterminate" indeterminate tone="neutral" size="large" />
</VStack>
);
}Weights
Deprecated Props
weight="default"와 weight="stronger"는 더 이상 사용되지 않습니다. 대신
weight="regular"와 weight="bold"를 사용하세요.
import { VStack } from "@seed-design/react";
import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox";
export default function CheckboxWeights() {
return (
<VStack p="x6">
<CheckboxGroup aria-label="Weight examples">
<Checkbox label="Regular Label Text" weight="regular" tone="neutral" size="large" />
<Checkbox label="Bold Label Text" weight="bold" tone="neutral" size="large" />
</CheckboxGroup>
</VStack>
);
}Long Label
import { VStack } from "@seed-design/react";
import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox";
export default function CheckboxLongLabel() {
return (
<VStack p="x6">
<CheckboxGroup aria-label="Long label examples">
<Checkbox
size="medium"
tone="neutral"
label="Consequat ut veniam aliqua deserunt occaecat enim occaecat veniam et et cillum nulla officia incididunt incididunt. Sint laboris labore occaecat fugiat culpa voluptate ullamco in elit dolore exercitation nulla."
/>
<Checkbox
size="large"
tone="neutral"
label="Consequat ut veniam aliqua deserunt occaecat enim occaecat veniam et et cillum nulla officia incididunt incididunt. Sint laboris labore occaecat fugiat culpa voluptate ullamco in elit dolore exercitation nulla."
/>
</CheckboxGroup>
</VStack>
);
}Disabled
import { VStack } from "@seed-design/react";
import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox";
export default function CheckboxDisabled() {
return (
<VStack p="x6">
<CheckboxGroup aria-label="Disabled examples">
<Checkbox
defaultChecked
label="Disabled Checked, Square"
disabled
tone="neutral"
size="large"
/>
<Checkbox
checked={false}
label="Disabled without Checked, Square"
disabled
tone="neutral"
size="large"
/>
<Checkbox
variant="ghost"
defaultChecked
label="Disabled Checked, Ghost"
disabled
tone="neutral"
size="large"
/>
<Checkbox
variant="ghost"
checked={false}
label="Disabled without Checked, Ghost"
disabled
tone="neutral"
size="large"
/>
</CheckboxGroup>
</VStack>
);
}Listening to Value Changes
onCheckedChange를 사용하여 체크박스의 선택 상태 변경을 감지할 수 있습니다.
이벤트를 활용해야 하는 경우 inputProps를 통해 내부 <input> 요소에 직접 이벤트 핸들러를 추가할 수 있습니다.
import { VStack, Text } from "@seed-design/react";
import { Checkbox } from "seed-design/ui/checkbox";
import { useState } from "react";
export default function CheckboxValueChanges() {
const [count, setCount] = useState(0);
const [lastValue, setLastValue] = useState<boolean | null>(null);
return (
<VStack gap="x4" align="center" p="x6">
<Checkbox
label="Click me"
tone="neutral"
size="large"
onCheckedChange={(checked) => {
setCount((prev) => prev + 1);
setLastValue(checked);
}}
/>
<Text>
onCheckedChange called: {count} times, last value: {`${lastValue ?? "-"}`}
</Text>
</VStack>
);
}Use Cases
React Hook Form
import { HStack, VStack } from "@seed-design/react";
import { useCallback, type FormEvent } from "react";
import { useController, useForm, type Control } from "react-hook-form";
import { ActionButton } from "seed-design/ui/action-button";
import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox";
const POSSIBLE_FRUIT_VALUES = ["apple", "melon", "mango"] as const;
type FormValues = Record<(typeof POSSIBLE_FRUIT_VALUES)[number], boolean>;
export default function CheckboxReactHookForm() {
const { handleSubmit, reset, setValue, control } = useForm<FormValues>({
defaultValues: {
apple: false,
melon: true,
mango: false,
},
});
const onValid = useCallback((data: FormValues) => {
window.alert(JSON.stringify(data, null, 2));
}, []);
const onReset = useCallback(
(event: FormEvent) => {
event.preventDefault();
reset();
},
[reset],
);
return (
<VStack gap="x3" p="x6" as="form" onSubmit={handleSubmit(onValid)} onReset={onReset}>
<CheckboxGroup aria-label="Fruit selection">
{POSSIBLE_FRUIT_VALUES.map((name) => (
<CheckboxItem key={name} name={name} control={control} />
))}
</CheckboxGroup>
<HStack gap="x2">
<ActionButton type="reset" variant="neutralWeak">
초기화
</ActionButton>
<ActionButton
type="button"
variant="neutralWeak"
flexGrow={1}
onClick={() => setValue("mango", true)}
>
mango 선택
</ActionButton>
<ActionButton type="submit" variant="neutralSolid" flexGrow={1}>
제출
</ActionButton>
</HStack>
</VStack>
);
}
interface CheckboxItemProps {
name: keyof FormValues;
control: Control<FormValues>;
}
function CheckboxItem({ name, control }: CheckboxItemProps) {
const {
field: { value, ...restProps },
fieldState: { invalid },
} = useController({ name, control });
return (
<Checkbox
key={name}
label={name}
checked={value}
inputProps={restProps}
invalid={invalid}
tone="neutral"
size="large"
/>
);
}Using Checkmark
Checkmark는 독립적인 체크 마크 컴포넌트로, Checkbox Primitive 컴포넌트와 조합하여 커스텀 레이아웃을 위해 사용할 수 있습니다.
import { HStack, Text, VStack } from "@seed-design/react";
import { Checkbox } from "@seed-design/react/primitive";
import { Checkmark } from "seed-design/ui/checkbox";
function CustomCheckbox({ children, ...props }: Checkbox.RootProps) {
return (
<VStack asChild gap="x2" align="center">
<Checkbox.Root {...props}>
<Checkmark tone="neutral" />
<Checkbox.HiddenInput />
{children}
</Checkbox.Root>
</VStack>
);
}
export default function CheckboxCheckmark() {
return (
<HStack gap="x6" p="x6">
<CustomCheckbox>
<Text textStyle="t7Regular">regular</Text>
</CustomCheckbox>
<CustomCheckbox defaultChecked>
<Text textStyle="t7Medium">medium</Text>
</CustomCheckbox>
<CustomCheckbox>
<Text textStyle="t7Bold">bold</Text>
</CustomCheckbox>
</HStack>
);
}Fieldset Integration
CheckboxGroup을 사용하여 여러 체크박스를 그룹화하고 label, description, errorMessage 등의 Fieldset 관련 prop을 사용할 수 있습니다.
import { ActionButton, HStack, VStack } from "@seed-design/react";
import { useState } from "react";
import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox";
export default function CheckboxFieldset() {
const [firstErrors, setFirstErrors] = useState<Record<string, string | undefined>>({});
const [secondErrors, setSecondErrors] = useState<Record<string, string | undefined>>({});
const handleFirstSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const fruits = formData.getAll("fruit");
if (fruits.includes("apple")) {
setFirstErrors({ apple: "Apple은 선택할 수 없습니다." });
return;
}
setFirstErrors({});
alert(JSON.stringify(fruits, null, 2));
};
const handleSecondSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const agreements = formData.getAll("agreement");
const hasTerms = agreements.includes("terms");
const hasPrivacy = agreements.includes("privacy");
if (!hasTerms || !hasPrivacy) {
setSecondErrors({
...(!hasTerms && { terms: "필수 항목에 동의해 주세요." }),
...(!hasPrivacy && { privacy: "필수 항목에 동의해 주세요." }),
});
return;
}
setSecondErrors({});
alert(JSON.stringify(agreements, null, 2));
};
return (
<HStack width="full" gap="x8" align="flex-start" p="x6">
<VStack asChild gap="spacingY.componentDefault" style={{ flex: 1 }}>
<form onSubmit={handleFirstSubmit}>
<CheckboxGroup
label="좋아하는 과일"
indicator="선택"
description="Apple을 선택하고 제출해보세요."
errorMessage={Object.values(firstErrors).filter(Boolean).join(", ")}
>
<Checkbox
label="Apple"
tone="neutral"
size="large"
defaultChecked
invalid={!!firstErrors.apple}
inputProps={{ name: "fruit", value: "apple" }}
/>
<Checkbox
label="Banana"
tone="neutral"
size="large"
invalid={!!firstErrors.banana}
inputProps={{ name: "fruit", value: "banana" }}
/>
<Checkbox
label="Orange"
tone="neutral"
size="large"
invalid={!!firstErrors.orange}
inputProps={{ name: "fruit", value: "orange" }}
/>
</CheckboxGroup>
<ActionButton type="submit" variant="neutralSolid">
제출
</ActionButton>
</form>
</VStack>
<VStack asChild gap="spacingY.componentDefault" style={{ flex: 1 }}>
<form onSubmit={handleSecondSubmit}>
<CheckboxGroup
label="약관 동의"
labelWeight="bold"
showRequiredIndicator
description="이용약관을 선택하지 않고 제출해보세요."
errorMessage={Object.values(secondErrors).filter(Boolean).join(", ")}
>
<Checkbox
label="이용약관 동의 (필수)"
tone="neutral"
size="large"
invalid={!!secondErrors.terms}
inputProps={{ name: "agreement", value: "terms" }}
/>
<Checkbox
label="개인정보 처리방침 동의 (필수)"
tone="neutral"
size="large"
defaultChecked
invalid={!!secondErrors.privacy}
inputProps={{ name: "agreement", value: "privacy" }}
/>
<Checkbox
label="마케팅 수신 동의 (선택)"
tone="neutral"
size="large"
invalid={!!secondErrors.marketing}
inputProps={{ name: "agreement", value: "marketing" }}
/>
</CheckboxGroup>
<ActionButton type="submit" variant="neutralSolid">
제출
</ActionButton>
</form>
</VStack>
</HStack>
);
}Last updated on