Select
트리거를 눌러 열리는 목록에서 값을 선택하는 컴포넌트입니다.
import { Box } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectPreview() {
return (
<Box width="240px">
<SelectRoot defaultValue={["apple"]}>
<SelectTrigger aria-label="과일" placeholder="과일을 선택하세요" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</Box>
);
}Installation
npx @seed-design/cli@latest add ui:selectpnpm dlx @seed-design/cli@latest add ui:selectyarn dlx @seed-design/cli@latest add ui:selectbun x @seed-design/cli@latest add ui:select의존성 설치
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:select
* @requires @seed-design/react@^2.1.0
* @requires @seed-design/css@^2.3.0
**/
"use client";
import {
Field as SeedField,
PrefixIcon,
Select as SeedSelect,
VisuallyHidden,
} from "@seed-design/react";
import type { FieldLabelVariantProps } from "@seed-design/css/recipes/field-label";
import {
IconCheckmarkFatFill,
IconChevronDownSmallLine,
IconExclamationmarkCircleFill,
} from "@karrotmarket/react-monochrome-icon";
import * as React from "react";
export interface SelectRootProps extends SeedSelect.RootProps {
label?: React.ReactNode;
/**
* @default "medium"
*/
labelWeight?: FieldLabelVariantProps["weight"];
indicator?: React.ReactNode;
showRequiredIndicator?: boolean;
description?: React.ReactNode;
errorMessage?: React.ReactNode;
hiddenSelectProps?: SeedSelect.HiddenSelectProps;
fieldRef?: React.Ref<HTMLDivElement>;
}
/**
* @see https://seed-design.io/react/components/select
*/
export const SelectRoot = ({
label,
labelWeight,
indicator,
showRequiredIndicator,
description,
errorMessage,
hiddenSelectProps,
fieldRef,
children,
...props
}: SelectRootProps) => {
const renderHeader = label || indicator;
const renderDescription = !!description;
const renderErrorMessage = errorMessage && props.invalid;
const renderFooter = renderDescription || renderErrorMessage;
if (process.env.NODE_ENV !== "production" && !label) {
console.warn(
"SelectRoot: Provide a `label` prop for better accessibility. Please ignore this warning if you've provided `aria-label` or `aria-labelledby` to the `SelectTrigger` inside. This warning will not be shown in production builds.",
);
}
return (
<SeedField.Root
required={props.required}
disabled={props.disabled}
invalid={props.invalid}
readOnly={props.readOnly}
name={props.name}
ref={fieldRef}
>
{renderHeader && (
<SeedField.Header>
<SeedField.Label weight={labelWeight}>
{label}
{showRequiredIndicator && <SeedField.RequiredIndicator />}
{indicator && <SeedField.IndicatorText>{indicator}</SeedField.IndicatorText>}
</SeedField.Label>
{/* You might want to put your custom element here */}
</SeedField.Header>
)}
<SeedSelect.Root {...props}>
{children}
<SeedSelect.HiddenSelect {...hiddenSelectProps} />
</SeedSelect.Root>
{renderFooter && (
<SeedField.Footer>
{renderDescription &&
(renderErrorMessage ? (
<VisuallyHidden asChild>
<SeedField.Description>{description}</SeedField.Description>
</VisuallyHidden>
) : (
<SeedField.Description>{description}</SeedField.Description>
))}
{renderErrorMessage && (
<SeedField.ErrorMessage>
<PrefixIcon svg={<IconExclamationmarkCircleFill />} />
{errorMessage}
</SeedField.ErrorMessage>
)}
</SeedField.Footer>
)}
</SeedField.Root>
);
};
export interface SelectTriggerProps extends Omit<SeedSelect.TriggerProps, "children"> {
placeholder?: React.ReactNode;
prefixIcon?: React.ReactNode;
/**
* @default <IconChevronDownSmallLine />
*/
suffixIcon?: React.ReactNode;
}
/**
* @see https://seed-design.io/react/components/select
*/
export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerProps>(
({ placeholder, prefixIcon, suffixIcon = <IconChevronDownSmallLine />, ...props }, ref) => {
return (
<SeedSelect.Trigger ref={ref} {...props}>
<SeedSelect.PrefixIcon fallback={prefixIcon} />
<SeedSelect.Value />
{placeholder && <SeedSelect.Placeholder>{placeholder}</SeedSelect.Placeholder>}
<SeedSelect.SuffixIcon svg={suffixIcon} />
</SeedSelect.Trigger>
);
},
);
SelectTrigger.displayName = "SelectTrigger";
export interface SelectContentProps extends SeedSelect.ContentProps {
positionerContainer?: SeedSelect.PositionerProps["container"];
}
/**
* @see https://seed-design.io/react/components/select
*/
export const SelectContent = React.forwardRef<HTMLDivElement, SelectContentProps>(
({ children, positionerContainer, ...props }, ref) => {
return (
<SeedSelect.Positioner container={positionerContainer}>
<SeedSelect.Content ref={ref} {...props}>
<SeedSelect.ScrollArea>{children}</SeedSelect.ScrollArea>
</SeedSelect.Content>
</SeedSelect.Positioner>
);
},
);
SelectContent.displayName = "SelectContent";
export interface SelectGroupProps extends SeedSelect.GroupProps {
label?: React.ReactNode;
}
/**
* @see https://seed-design.io/react/components/select
*/
export const SelectGroup = React.forwardRef<HTMLDivElement, SelectGroupProps>(
({ label, children, ...props }, ref) => {
return (
<SeedSelect.Group ref={ref} {...props}>
{label && <SeedSelect.GroupLabel>{label}</SeedSelect.GroupLabel>}
{children}
</SeedSelect.Group>
);
},
);
SelectGroup.displayName = "SelectGroup";
export interface SelectItemProps extends Omit<SeedSelect.ItemProps, "children"> {
description?: React.ReactNode;
}
/**
* @see https://seed-design.io/react/components/select
*/
export const SelectItem = React.forwardRef<HTMLDivElement, SelectItemProps>(
({ description, ...props }, ref) => {
return (
<SeedSelect.Item ref={ref} {...props}>
<SeedSelect.ItemPrefixIcon />
<SeedSelect.ItemBody>
<SeedSelect.ItemLabel />
{description && <SeedSelect.ItemDescription>{description}</SeedSelect.ItemDescription>}
</SeedSelect.ItemBody>
<SeedSelect.ItemIndicator selected={<IconCheckmarkFatFill />} />
</SeedSelect.Item>
);
},
);
SelectItem.displayName = "SelectItem";
/**
* 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.
*/
Usage
import {
SelectRoot,
SelectTrigger,
SelectContent,
SelectGroup,
SelectItem,
} from "seed-design/ui/select";<SelectRoot defaultValue={["apple"]}>
<SelectTrigger aria-label="과일" placeholder="과일을 선택하세요" />
<SelectContent>
<SelectGroup label="국산">
<SelectItem value="apple" label="사과" />
<SelectItem value="pear" label="배" />
</SelectGroup>
<SelectGroup label="수입산">
<SelectItem value="banana" label="바나나" />
</SelectGroup>
</SelectContent>
</SelectRoot>SelectRoot: 선택 값(value)과 열림 상태를 관리합니다.SelectTrigger: 선택된 값 또는 placeholder를 표시하며, 클릭 시 목록을 엽니다.SelectContent: 옵션 목록을 감싸는 플로팅 컨테이너입니다.SelectGroup: 관련된 옵션들을 그룹으로 묶습니다. 모든SelectItem은SelectGroup안에 있어야 합니다.label로 그룹의 제목을 표시할 수 있습니다.SelectItem: 개별 옵션입니다.value가 필요하며, 선택되면 체크마크가 표시됩니다.
Props
SelectRoot
Prop
Type
label?React.ReactNodeindicator?React.ReactNodeshowRequiredIndicator?boolean | undefineddescription?React.ReactNodeerrorMessage?React.ReactNodehiddenSelectProps?SeedSelect.HiddenSelectProps | undefinedfieldRef?React.Ref<HTMLDivElement> | undefinedchildren?React.ReactNodeopen?boolean | undefineddefaultOpen?boolean | undefinedonOpenChange?((open: boolean, details?: SelectOpenChangeDetails) => void) | undefineddefaultValue?string[] | undefinedonValueChange?((value: string[]) => void) | undefineddisabled?boolean | undefinedinvalid?boolean | undefinedreadOnly?boolean | undefinedSelectTrigger
Prop
Type
placeholder?React.ReactNodeprefixIcon?React.ReactNodeSelectContent
Prop
Type
positionerContainer?React.RefObject<HTMLElement | null> | undefinedSelectGroup
Prop
Type
label?React.ReactNodeSelectItem
Prop
Type
description?React.ReactNodedisabled?boolean | undefinedvaluestringExamples
Size
size로 트리거와 목록의 크기를 정합니다. (default: large)
responsive 사용 시 화면 너비에 따라 size가 자동으로 전환됩니다.
import { VStack } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectSize() {
return (
<VStack gap="x4" width="240px">
<SelectRoot size="large" defaultValue={["apple"]}>
<SelectTrigger aria-label="과일 (large)" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
</SelectGroup>
</SelectContent>
</SelectRoot>
<SelectRoot size="medium" defaultValue={["apple"]}>
<SelectTrigger aria-label="과일 (medium)" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</VStack>
);
}Groups
SelectGroup으로 옵션을 묶고, label로 그룹의 제목을 표시합니다. SelectGroup의 개수와 관계없이 모든 SelectItem은 SelectGroup 안에 있어야 합니다.
그룹이 두 개 이상이면 그룹 사이에 구분선이 자동으로 그려집니다.
import { Box } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectGroups() {
return (
<Box width="240px">
<SelectRoot defaultValue={["seoul"]}>
<SelectTrigger aria-label="지역" placeholder="지역 선택" />
<SelectContent>
<SelectGroup label="아시아">
<SelectItem value="seoul" label="서울" />
<SelectItem value="tokyo" label="도쿄" />
<SelectItem value="singapore" label="싱가포르" />
<SelectItem value="dubai" label="두바이" />
</SelectGroup>
<SelectGroup label="유럽">
<SelectItem value="london" label="런던" />
<SelectItem value="paris" label="파리" />
<SelectItem value="berlin" label="베를린" />
</SelectGroup>
<SelectGroup label="아메리카">
<SelectItem value="new-york" label="뉴욕" />
<SelectItem value="sao-paulo" label="상파울루" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</Box>
);
}Multiple Selection
SelectRoot에 multiple을 지정하면 여러 옵션을 선택할 수 있습니다. 옵션을 선택해도 목록이 닫히지 않으며, 이미 선택된 옵션을 다시 누르면 선택이 해제됩니다.
트리거에는 선택된 옵션들의 textValue가 ", "로 이어져 표시됩니다. 이 문구는 Custom Value Format으로 바꿀 수 있습니다.
import { Box } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectMultiple() {
return (
<Box width="240px">
<SelectRoot multiple defaultValue={["apple", "cherry"]}>
<SelectTrigger aria-label="과일" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
<SelectItem value="grape" label="포도" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</Box>
);
}With Description
SelectItem의 description prop으로 옵션에 부가 설명을 추가합니다.
import { Box } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectWithDescription() {
return (
<Box width="280px">
<SelectRoot defaultValue={["standard"]}>
<SelectTrigger aria-label="배송 방법" placeholder="배송 방법 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="standard" label="일반 배송" description="3-5일 소요" />
<SelectItem value="express" label="빠른 배송" description="1-2일 소요" />
<SelectItem value="same-day" label="당일 배송" description="오늘 도착" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</Box>
);
}With Prefix Icon
SelectItem의 prefixIcon prop으로 옵션에 아이콘을 표시합니다.
1개의 옵션이 선택된 경우 해당 옵션의 아이콘이 트리거의 prefix 아이콘으로 표시됩니다. 선택된 옵션이 없거나, 선택된 옵션에 아이콘이 없거나, 여러 옵션이 선택된 경우 트리거에는 SelectTrigger에 지정한 prefixIcon이 표시됩니다.
import {
IconGlobeLine,
IconLockLine,
IconPerson2Line,
IconPersonLine,
} from "@karrotmarket/react-monochrome-icon";
import { Box } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectWithPrefixIcon() {
return (
<Box width="280px">
<SelectRoot>
<SelectTrigger
aria-label="공유 대상"
placeholder="공유 대상"
prefixIcon={<IconPerson2Line />}
/>
<SelectContent>
<SelectGroup label="그룹">
<SelectItem value="public" label="전체 공개" prefixIcon={<IconGlobeLine />} />
<SelectItem value="followers" label="팔로워만" prefixIcon={<IconLockLine />} />
<SelectItem value="private" label="나만" prefixIcon={<IconPersonLine />} />
</SelectGroup>
<SelectGroup label="사람">
<SelectItem value="kim" label="김하늘" />
<SelectItem value="lee" label="이하늘" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</Box>
);
}Custom Item Label
SelectItem의 label에는 ReactNode를 넘길 수 있습니다. 이 노드는 목록의 옵션에만 렌더링됩니다.
SelectItem의 label로 string이 아닌 ReactNode를 지정하는 경우 트리거에는 SelectItem의 textValue가 표시됩니다. textValue는 label이 string이면 label, 아니면 value입니다.
따라서, label이 string이 아닌 경우 textValue를 함께 지정하는 것을 권장합니다. textValue는 트리거에 표시되는 문구, 키보드로 타이핑해 옵션을 찾을 때 매칭되는 문자열(typeahead), 폼 제출용 native <option>의 텍스트로 쓰입니다. label로 string이 아닌 ReactNode를 전달하면서 textValue를 지정하지 않는 경우 이 값들은 모두 기본적으로 value로 설정되며, 개발 모드에서 경고가 출력됩니다.
typeahead 문자열만 변경하고자 하는 경우 typeaheadLabel을 사용하세요.
import {
IconCarLine,
IconFigureBikeLine,
IconMetroFrontsideLine,
} from "@karrotmarket/react-monochrome-icon";
import { Badge, Box, HStack } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectCustomLabel() {
return (
<Box width="280px">
<SelectRoot defaultValue={["metro"]}>
<SelectTrigger aria-label="이동 수단" placeholder="이동 수단 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="bike" label="자전거" prefixIcon={<IconFigureBikeLine />} />
<SelectItem
value="metro"
textValue="지하철"
prefixIcon={<IconMetroFrontsideLine />}
label={
<HStack as="span" align="center" gap="x1_5">
지하철
<Badge variant="weak" tone="informative">
가장 빠름
</Badge>
</HStack>
}
/>
<SelectItem
value="car"
textValue="자동차"
disabled
prefixIcon={<IconCarLine />}
label={
<HStack as="span" align="center" gap="x1_5">
자동차
<Badge variant="weak" tone="warning">
고객지원에 문의
</Badge>
</HStack>
}
/>
</SelectGroup>
</SelectContent>
</SelectRoot>
</Box>
);
}Disabled
SelectItem의 disabled로 특정 옵션을, SelectRoot의 disabled로 Select 전체를 비활성화합니다.
import { VStack } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectDisabled() {
return (
<VStack gap="x4" width="240px">
<SelectRoot defaultValue={["apple"]}>
<SelectTrigger aria-label="과일" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" disabled />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
<SelectRoot disabled defaultValue={["apple"]}>
<SelectTrigger aria-label="비활성 과일" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</VStack>
);
}Long List
옵션이 많으면 목록 안에서 스크롤됩니다. 목록의 최대 높이는 지정된 최대 높이와 트리거 주변에 남은 화면 공간 중 더 작은 값으로 정해지고, 노치와 홈 인디케이터 영역을 피해 배치됩니다.
목록은 현재 선택된 옵션이 보이는 위치로 스크롤된 상태에서 열립니다.
import { Box } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
const TIME_SLOTS = Array.from({ length: 48 }, (_, index) => {
const hour = String(Math.floor(index / 2)).padStart(2, "0");
const minute = index % 2 === 0 ? "00" : "30";
return `${hour}:${minute}`;
});
export default function SelectLongList() {
return (
<Box width="240px">
<SelectRoot defaultValue={["14:00"]}>
<SelectTrigger aria-label="예약 시간" placeholder="시간 선택" />
<SelectContent>
<SelectGroup>
{TIME_SLOTS.map((slot) => (
<SelectItem key={slot} value={slot} label={slot} />
))}
</SelectGroup>
</SelectContent>
</SelectRoot>
</Box>
);
}Placement
SelectRoot의 placement prop으로 목록의 위치를 설정합니다. 기본값은 "bottom"입니다.
import { Box } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectPlacement() {
return (
<Box width="240px">
<SelectRoot placement="top" defaultValue={["apple"]}>
<SelectTrigger aria-label="과일" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</Box>
);
}Listening to Value Changes
value와 onValueChange로 선택 값을 제어할 수 있습니다.
import { VStack } from "@seed-design/react";
import * as React from "react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectControlled() {
const [value, setValue] = React.useState<string[]>(["apple"]);
return (
<VStack gap="x2" width="240px">
<SelectRoot value={value} onValueChange={setValue}>
<SelectTrigger aria-label="과일" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
<span>선택된 값: {value.length > 0 ? value.join(", ") : "없음"}</span>
</VStack>
);
}Controlled Open State
open과 onOpenChange로 목록의 열림 상태를 제어할 수 있습니다. 초기 상태만 지정하려면 defaultOpen을 사용하세요.
목록이 열리면 포커스가 목록으로 이동하고, 닫히면 트리거로 되돌아옵니다.
import { VStack } from "@seed-design/react";
import * as React from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectControlledOpen() {
const [open, setOpen] = React.useState(false);
return (
<VStack gap="x2" width="240px">
<SelectRoot open={open} onOpenChange={setOpen} defaultValue={["apple"]}>
<SelectTrigger aria-label="과일" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
<ActionButton variant="neutralWeak" disabled={open} onClick={() => setOpen(true)}>
목록 열기
</ActionButton>
<span>목록 상태: {open ? "열림" : "닫힘"}</span>
</VStack>
);
}onOpenChange Details
onOpenChange 두 번째 인자로 details가 제공됩니다.
reason
열릴 때 (open: true)
"trigger":SelectTrigger클릭 또는SelectTrigger에 포커스된 상태에서 ↓, ↑, Enter, Space 키 사용
닫힐 때 (open: false)
"trigger": 목록이 열린 상태에서SelectTrigger클릭"itemSelect": 옵션 선택multiple인 경우 옵션을 선택해도 목록이 닫히지 않으므로 발생하지 않습니다.
"keyboardClose": 하이라이트된 옵션이 없는 상태에서 Enter 또는 Space 키 사용"escapeKeyDown": ESC 키 사용"interactOutside": 외부 영역 클릭"focusOut": Tab 등으로 포커스가 목록 밖으로 이동"cascadeDismiss": 상위 레이어 닫힘으로 인한 연쇄 닫힘
import { Box, HStack, Text, VStack } from "@seed-design/react";
import { useState } from "react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectOpenChangeReason() {
const [open, setOpen] = useState(false);
const [openReason, setOpenReason] = useState<string | null>(null);
const [closeReason, setCloseReason] = useState<string | null>(null);
return (
<VStack gap="x4" align="center">
<Box width="240px">
<SelectRoot
open={open}
onOpenChange={(open, details) => {
setOpen(open);
(open ? setOpenReason : setCloseReason)(details?.reason ?? null);
}}
defaultValue={["apple"]}
>
<SelectTrigger aria-label="과일" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</Box>
<HStack gap="x4">
<Text fontSize="t3" color="fg.neutralMuted">
마지막 열림 이유: {openReason ?? "-"}
</Text>
<Text fontSize="t3" color="fg.neutralMuted">
마지막 닫힘 이유: {closeReason ?? "-"}
</Text>
</HStack>
</VStack>
);
}Custom Value Format
SelectRoot의 formatValue prop으로 트리거에 표시되는 선택 값을 커스텀합니다. 기본적으로는, 선택된 옵션들의 textValue를 ", "로 join하여 표시합니다. textValue의 기본값은 label이 string이면 label, 아닌 경우 value입니다.
formatValue가 받는 각 항목에는 label도 담겨 있으므로, ReactNode label을 트리거에 렌더링하고 싶다면 여기서 꺼내 쓰면 됩니다.
// 선택된 옵션 중 첫 번째 옵션의 label만 트리거에 표시
<SelectRoot formatValue={(items) => items[0]?.label} />Intl.ListFormat을 사용하면 locale에 맞는 접속사로 목록을 조합할 수 있습니다.
import { HStack } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
const listFormat = new Intl.ListFormat("ko", { type: "conjunction" });
export default function SelectFormatValue() {
return (
<HStack gap="x4" width="full">
<SelectRoot
multiple
defaultValue={["apple", "banana"]}
formatValue={(items) => listFormat.format(items.map((item) => item.textValue))}
>
<SelectTrigger aria-label="과일" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
<SelectRoot
multiple
defaultValue={["apple", "banana", "cherry"]}
formatValue={([first, ...rest]) =>
rest.length > 0 ? `${first.textValue} 외 ${rest.length}개` : first.textValue
}
>
<SelectTrigger aria-label="과일" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</HStack>
);
}Field Integration
label, description, errorMessage 등의 Field 관련 prop을 사용할 수 있습니다.
import { VStack } from "@seed-design/react";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectField() {
return (
<VStack gap="x6" width="240px">
<SelectRoot
label="과일"
description="가장 좋아하는 과일을 선택하세요."
defaultValue={["apple"]}
>
<SelectTrigger placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
<SelectRoot label="과일" labelWeight="bold" invalid errorMessage="과일을 선택해주세요.">
<SelectTrigger placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</VStack>
);
}Form Integration
SelectRoot가 내부에서 렌더링하는 숨겨진 native <select>를 통해 폼 제출에 참여할 수 있습니다.
import { VStack } from "@seed-design/react";
import * as React from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
export default function SelectForm() {
const [submitted, setSubmitted] = React.useState<string | null>(null);
return (
<VStack asChild gap="x2" width="240px">
<form
onSubmit={(event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
setSubmitted(String(data.get("fruit")));
}}
>
<SelectRoot name="fruit" required>
<SelectTrigger aria-label="과일" placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
<ActionButton type="submit" variant="neutralSolid">
제출
</ActionButton>
{submitted && <span>제출된 값: {submitted}</span>}
</form>
</VStack>
);
}React Hook Form
value, onValueChange, invalid, errorMessage를 연결하면 React Hook Form의 useController로 검증 상태를 제어할 수 있습니다.
import { HStack, VStack } from "@seed-design/react";
import { useCallback, type FormEvent } from "react";
import { useController, useForm } from "react-hook-form";
import { ActionButton } from "seed-design/ui/action-button";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectRoot,
SelectTrigger,
} from "seed-design/ui/select";
interface FormValues {
fruit: string[];
}
export default function SelectReactHookForm() {
const { handleSubmit, reset, control } = useForm<FormValues>({
reValidateMode: "onSubmit",
defaultValues: {
fruit: [],
},
});
// SelectRoot is not a forwardRef component, so pass only the field props it
// accepts instead of spreading `...field` (which carries a `ref`).
const {
field: { value, onChange, name },
fieldState,
} = useController({
name: "fruit",
control,
rules: {
validate: (value) => value.length > 0 || "과일을 선택해주세요",
},
});
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" width="full" as="form" onSubmit={handleSubmit(onValid)} onReset={onReset}>
<SelectRoot
name={name}
label="과일"
description="가장 좋아하는 과일을 선택하세요"
invalid={fieldState.invalid}
errorMessage={fieldState.error?.message}
value={value}
onValueChange={onChange}
showRequiredIndicator
>
<SelectTrigger placeholder="과일 선택" />
<SelectContent>
<SelectGroup>
<SelectItem value="apple" label="사과" />
<SelectItem value="banana" label="바나나" />
<SelectItem value="cherry" label="체리" />
</SelectGroup>
</SelectContent>
</SelectRoot>
<HStack gap="x2">
<ActionButton type="reset" variant="neutralWeak">
초기화
</ActionButton>
<ActionButton type="submit" variant="neutralSolid" flexGrow={1}>
제출
</ActionButton>
</HStack>
</VStack>
);
}Last updated on