Quantity Picker, Time Picker, Date Picker, Select, Dialog, ScrollAutoHide
버튼으로 정수 단위의 수량을 한 단계씩 늘리거나 줄입니다. 최소·최대 범위와 삭제 가능한 상태를 표현하고 Form 값으로 제출할 수 있습니다. @seed-design/react@2.1.0, @seed-design/css@2.3.0부터 제공합니다.
주요 기능
최소·최대 수량을 정하고 controlled 또는 uncontrolled 방식으로 값을 관리할 수 있습니다.
최소 수량에서 감소 버튼을 삭제 버튼으로 바꾸고, 삭제 후 되돌리기 같은 흐름을 만들 수 있습니다.
값 텍스트와 버튼 배치를 바꾸거나 로딩 상태를 표시할 수 있습니다.
숨겨진 input을 통해 네이티브 Form 값으로 제출할 수 있습니다.
기본 사용
import { QuantityPicker } from "seed-design/ui/quantity-picker" ;
export default function QuantityPickerPreview () {
return < QuantityPicker min = { 1 } max = { 99 } defaultValue = { 1 } aria-label = "상품 수량" />;
}
최소 수량에서 항목 삭제하기
import { Text, VStack } from "@seed-design/react" ;
import { useState } from "react" ;
import { ActionButton } from "seed-design/ui/action-button" ;
import { QuantityPicker } from "seed-design/ui/quantity-picker" ;
export default function QuantityPickerRemovable () {
const [ removed , setRemoved ] = useState ( false );
if (removed) {
return (
< VStack gap = "x3" align = "center" >
< Text textStyle = "t4Regular" >상품을 삭제했습니다.</ Text >
< ActionButton variant = "neutralWeak" onClick = {() => setRemoved ( false )}>
되돌리기
</ ActionButton >
</ VStack >
);
}
return (
< VStack gap = "x3" align = "center" >
< QuantityPicker
min = { 1 }
max = { 99 }
defaultValue = { 1 }
removable
aria-label = "상품 수량"
removeAriaLabel = "상품 삭제"
onRemove = {() => setRemoved ( true )}
/>
< Text textStyle = "t4Regular" >최소 수량에서 Decrement 버튼이 Remove 버튼으로 전환됩니다.</ Text >
</ VStack >
);
}
Quantity Picker 디자인 가이드 · React 문서
휠을 굴려 오전·오후와 시·분을 선택합니다. locale에 따른 칼럼 순서와 분 단위 간격을 설정할 수 있습니다. @seed-design/react@2.2.0, @seed-design/css@2.4.0부터 제공합니다.
주요 기능
locale에 맞춰 오전·오후와 시·분 칼럼의 순서와 레이블을 바꿉니다.
minuteStep으로 선택 간격을 정하고 필요한 칼럼을 비활성화할 수 있습니다.
controlled 값이나 JavaScript Date 객체와 연결할 수 있습니다.
인라인으로 배치하거나 Bottom Sheet 안에서 값을 확인한 뒤 확정하는 흐름을 만들 수 있습니다.
기본 사용
import { Box, TimePicker } from "@seed-design/react" ;
export default function TimePickerPreview () {
return (
< Box width = "358px" maxWidth = "100%" >
< TimePicker defaultValue = {{ hour: 10 , minute: 30 }} />
</ Box >
);
}
Bottom Sheet에서 시간 확정하기
"use client" ;
import { Portal, TimePicker, type TimePickerValue } from "@seed-design/react" ;
import { ActionButton } from "seed-design/ui/action-button" ;
import {
BottomSheetBody,
BottomSheetContent,
BottomSheetFooter,
BottomSheetRoot,
} from "seed-design/ui/bottom-sheet" ;
import { FieldButton, FieldButtonValue } from "seed-design/ui/field-button" ;
import * as React from "react" ;
function formatTime ({ hour , minute } : TimePickerValue ) {
const period = hour < 12 ? "오전" : "오후" ;
const displayHour = hour % 12 || 12 ;
return `${ period } ${ displayHour }:${ String ( minute ). padStart ( 2 , "0" ) }` ;
}
export default function TimePickerBottomSheet () {
const [ open , setOpen ] = React. useState ( false );
const [ value , setValue ] = React. useState < TimePickerValue >({ hour: 10 , minute: 30 });
const [ draft , setDraft ] = React. useState (value);
const handleOpenChange = ( nextOpen : boolean ) => {
if (nextOpen) setDraft (value);
setOpen (nextOpen);
};
return (
< BottomSheetRoot open = {open} onOpenChange = {handleOpenChange}>
< FieldButton
label = "영업 시작 시간"
values = {[ formatTime (value)]}
style = {{ width: "240px" , maxWidth: "100%" }}
buttonProps = {{
"aria-label" : `영업 시작 시간, ${ formatTime ( value ) }` ,
"aria-haspopup" : "dialog" ,
"aria-expanded" : open,
onClick : () => handleOpenChange ( true ),
}}
>
< FieldButtonValue >{ formatTime (value)}</ FieldButtonValue >
</ FieldButton >
< Portal >
< BottomSheetContent title = "시간 선택" showCloseButton = { false } showHandle >
< BottomSheetBody paddingX = "x4" >
< TimePicker value = {draft} onValueChange = {setDraft} />
</ BottomSheetBody >
< BottomSheetFooter >
< ActionButton
variant = "neutralSolid"
onClick = {() => {
setValue (draft);
setOpen ( false );
}}
>
완료
</ ActionButton >
</ BottomSheetFooter >
</ BottomSheetContent >
</ Portal >
</ BottomSheetRoot >
);
}
Time Picker 디자인 가이드 · React 문서
달력에서 하나의 날짜, 날짜 범위 또는 여러 날짜를 선택합니다. 한 달과 두 달 레이아웃을 제공해 화면 크기와 선택 맥락에 맞게 구성할 수 있습니다. @seed-design/react@2.2.0, @seed-design/css@2.4.0부터 제공합니다.
주요 기능
단일 날짜, 날짜 범위, 여러 날짜 선택 방식을 제공합니다.
한 달·두 달·한 주·연속 스크롤 레이아웃으로 화면과 탐색 맥락에 맞게 구성합니다.
최소·최대 기간과 예약 완료일 같은 선택 제약 조건을 조합할 수 있습니다.
날짜 셀에 가격이나 예약 상태를 더하고, Time Picker와 날짜·시간 선택 흐름을 만들 수 있습니다.
기본 사용
import { Box, DatePicker } from "@seed-design/react" ;
export default function DatePickerPreview () {
return (
< Box width = "358px" maxWidth = "100%" >
< DatePicker
today = {{ year: 2026 , month: 7 , day: 30 }}
defaultValue = {{ year: 2026 , month: 7 , day: 30 }}
/>
</ Box >
);
}
예약 가능한 날짜 범위 선택하기
"use client" ;
import {
Box,
Text,
TwoMonthDatePicker,
VStack,
excludeDates,
rangeDayCountAtLeast,
rangeDayCountAtMost,
type DatePickerDate,
type DatePickerRangeValue,
} from "@seed-design/react" ;
import * as React from "react" ;
const bookedDateKeys = new Set ([ "2026-07-18" , "2026-07-19" , "2026-07-25" ]);
function toDateKey ( date : DatePickerDate ) {
return `${ date . year }-${ String ( date . month ). padStart ( 2 , "0" ) }-${ String ( date . day ). padStart ( 2 , "0" ) }` ;
}
const constraints = [
rangeDayCountAtLeast ( 1 ),
rangeDayCountAtMost ( 14 ),
excludeDates (( date ) => bookedDateKeys. has ( toDateKey (date))),
];
export default function DatePickerReservation () {
const [ value , setValue ] = React. useState < DatePickerRangeValue >({
start: { year: 2026 , month: 7 , day: 10 },
});
return (
< VStack gap = "x3" align = "center" >
< Box width = "720px" maxWidth = "100%" >
< TwoMonthDatePicker
selectionMode = "range"
today = {{ year: 2026 , month: 7 , day: 1 }}
value = {value}
onValueChange = {setValue}
constraints = {constraints}
aria-label = "체크인 및 체크아웃 날짜"
/>
</ Box >
< Text >
체크인 { toDateKey (value.start)}
{value.end ? ` · 체크아웃 ${ toDateKey ( value . end ) }` : " · 체크아웃을 선택하세요" }
</ Text >
< VStack gap = "x1" align = "center" >
< Text textStyle = "t2Regular" color = "fg.neutralMuted" >
선택 구간은 체크인·체크아웃을 포함해 1~14일이어야 합니다.
</ Text >
< Text textStyle = "t2Regular" color = "fg.neutralMuted" >
예약 완료일(7월 18일·19일·25일)이 포함된 구간은 선택할 수 없습니다.
</ Text >
</ VStack >
</ VStack >
);
}
Date Picker 디자인 가이드 · React 문서
Select는 트리거를 눌러 연 목록에서 하나 또는 여러 값을 선택합니다. 항목을 그룹으로 나누거나 설명과 아이콘을 더할 수 있고, 선택값을 Form으로 제출할 수 있습니다. @seed-design/react@2.1.0, @seed-design/css@2.3.0부터 제공합니다.
주요 기능
하나 또는 여러 값을 선택하고 선택된 값을 원하는 형식으로 표시할 수 있습니다.
항목을 그룹으로 나누고 설명, 아이콘, 커스텀 레이블을 더할 수 있습니다.
긴 목록의 스크롤, 팝오버 위치, 크기와 비활성 상태를 설정할 수 있습니다.
Field, 네이티브 Form, React Hook Form과 연결하고 열림 상태와 변경 이유를 제어할 수 있습니다.
기본 사용
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 >
);
}
React Hook Form으로 검증하기
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 >
);
}
Select 디자인 가이드 · React 문서
Dialog는 화면 중앙에서 사용자의 주의를 모으고, 제목·본문·푸터에 폼이나 스크롤되는 콘텐츠를 담습니다. ResponsiveDialog를 사용하면 넓은 화면에서는 Dialog, 작은 화면에서는 Bottom Sheet로 자동 전환됩니다. @seed-design/react@2.1.0, @seed-design/css@2.3.0부터 제공합니다.
주요 기능
크기, 닫기 버튼, 본문 높이와 패딩, 푸터 액션 배치를 콘텐츠에 맞게 구성할 수 있습니다.
controlled 상태와 Portal을 사용해 열림 상태와 렌더링 위치를 관리할 수 있습니다.
바깥 클릭이나 ESC 입력을 막고, 닫기 이유에 따라 확인 Dialog를 한 번 더 보여줄 수 있습니다.
ResponsiveDialog는 넓은 화면의 Dialog와 작은 화면의 Bottom Sheet를 자동으로 전환합니다.
기본 사용
import { HStack, Text } from "@seed-design/react" ;
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
} from "seed-design/ui/dialog" ;
import { ActionButton } from "seed-design/ui/action-button" ;
const DialogPreview = () => {
return (
< DialogRoot >
< DialogTrigger asChild >
< ActionButton variant = "neutralSolid" >Open Dialog</ ActionButton >
</ DialogTrigger >
< DialogContent title = "제목" description = "설명을 작성할 수 있어요" >
< DialogBody >
< Text textStyle = "articleBody" >
본문에는 사용자가 확인해야 할 내용이나 추가 입력 폼을 배치할 수 있습니다.
</ Text >
</ DialogBody >
< DialogFooter >
< HStack gap = "x2" justify = "flex-end" >
< DialogAction variant = "neutralWeak" >취소</ DialogAction >
< DialogAction variant = "neutralSolid" >확인</ DialogAction >
</ HStack >
</ DialogFooter >
</ DialogContent >
</ DialogRoot >
);
};
export default DialogPreview;
화면 크기에 맞춰 Dialog와 Bottom Sheet 전환하기
import { HStack, Text, useResponsiveDialogContext } from "@seed-design/react" ;
import { forwardRef } from "react" ;
import { ActionButton } from "seed-design/ui/action-button" ;
import {
ResponsiveDialogAction,
ResponsiveDialogBody,
ResponsiveDialogContent,
ResponsiveDialogFooter,
ResponsiveDialogRoot,
ResponsiveDialogTrigger,
type ResponsiveDialogFooterProps,
} from "seed-design/ui/responsive-dialog" ;
const Footer = forwardRef < HTMLDivElement , ResponsiveDialogFooterProps >(( props , ref ) => {
const { shouldUseBottomSheet } = useResponsiveDialogContext ();
return (
< ResponsiveDialogFooter ref = {ref} { ... props}>
< HStack gap = "x2" justify = "flex-end" >
< ResponsiveDialogAction
variant = "neutralWeak"
flexGrow = {shouldUseBottomSheet ? 1 : undefined }
>
취소
</ ResponsiveDialogAction >
< ResponsiveDialogAction
variant = "neutralSolid"
flexGrow = {shouldUseBottomSheet ? 1 : undefined }
>
확인
</ ResponsiveDialogAction >
</ HStack >
</ ResponsiveDialogFooter >
);
});
const DialogResponsive = () => {
return (
< ResponsiveDialogRoot >
< ResponsiveDialogTrigger asChild >
< ActionButton variant = "neutralSolid" >Open</ ActionButton >
</ ResponsiveDialogTrigger >
< ResponsiveDialogContent
title = "반응형 다이얼로그"
description = "화면 크기에 따라 적합한 컴포넌트로 자동 전환됩니다."
>
< ResponsiveDialogBody >
< Text textStyle = "articleBody" >
md 이상에서는 화면 중앙의 Dialog로, sm 이하에서는 화면 하단에서 슬라이드되는 Bottom
Sheet로 표시됩니다.
</ Text >
</ ResponsiveDialogBody >
< Footer />
</ ResponsiveDialogContent >
</ ResponsiveDialogRoot >
);
};
export default DialogResponsive;
Dialog 디자인 가이드 · Dialog·ResponsiveDialog React 문서
스크롤 방향에 따라 필터, 탭, 보조 헤더 같은 고정 영역을 자연스럽게 숨기고 드러냅니다. 아래로 스크롤하면 영역이 숨겨지고, 위로 올리면 이동한 거리만큼 다시 나타납니다. Breeze 스니펫으로 제공합니다.
주요 기능
터치와 트랙패드 스크롤을 모두 추적합니다.
스크롤이 끝나면 SEED easing으로 영역을 완전히 숨기거나 드러냅니다.
Safari의 상·하단 탄성 스크롤이 잘못된 노출로 이어지지 않도록 처리합니다.
숨겨진 영역으로 키보드 포커스가 이동하면 즉시 다시 보여주고, 모션 감소 설정에서는 자동 숨김을 비활성화합니다.
동작 예시
"use client" ;
import { IconChevronDownFill } from "@karrotmarket/react-monochrome-icon" ;
import { Badge, Box, Flex, HStack, Icon, Text, VStack } from "@seed-design/react" ;
import { useRef } from "react" ;
import { ScrollAutoHide } from "seed-design/breeze/scroll-auto-hide/scroll-auto-hide" ;
import { Avatar } from "seed-design/ui/avatar" ;
import { Chip } from "seed-design/ui/chip" ;
import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder" ;
import { TabsList, TabsRoot, TabsTrigger } from "seed-design/ui/tabs" ;
import { TagGroupItem, TagGroupRoot } from "seed-design/ui/tag-group" ;
type Article = {
author : string ;
title : string ;
content : string ;
category : string ;
date : string ;
popular ?: boolean ;
};
const ARTICLES = [
{
author: "magna" ,
title: "독서 습관 만들기" ,
content: "하루 10페이지부터 시작하자. 취침 전 20분 독서는 수면의 질도 높여줘요." ,
category: "라이프스타일" ,
date: "12월 4일" ,
popular: true ,
},
{
author: "ullamco" ,
title: "알프스 하이킹 가이드" ,
content: "그린델발트에서 시작해 아이거 북벽을 감상하는 초보자용 코스를 소개해요." ,
category: "여행" ,
date: "10월 15일" ,
},
{
author: "consectetur" ,
title: "커피 브루잉 팁" ,
content: "물 온도 92도, 중간 분쇄도와 3분의 추출 시간이 기본이에요." ,
category: "음식" ,
date: "9월 25일" ,
},
{
author: "exercitation" ,
title: "실내 공기 정화 식물" ,
content: "관리하기 쉽고 공기 정화 능력이 뛰어난 식물을 모아봤어요." ,
category: "라이프스타일" ,
date: "8월 7일" ,
},
{
author: "elit" ,
title: "아침 루틴의 힘" ,
content: "따뜻한 물 한 잔과 가벼운 스트레칭으로 하루를 시작해보세요." ,
category: "라이프스타일" ,
date: "7월 28일" ,
popular: true ,
},
] satisfies Article [];
const FILTERS = [ "카테고리" , "동네" , "작성자" , "작성 시간" ];
export default function ScrollAutoHidePreview () {
const scrollContainerRef = useRef < HTMLDivElement >( null );
return (
< Box
width = "360px"
borderWidth = { 1 }
borderColor = "stroke.neutralMuted"
borderRadius = "r3"
bg = "bg.layerDefault"
overflowX = "hidden"
overflowY = "hidden"
>
< Box as = "header" px = "spacingX.globalGutter" py = "x4" >
< Text textStyle = "t6Bold" color = "fg.neutral" >
Demo
</ Text >
</ Box >
< TabsRoot defaultValue = "recommendations" size = "medium" triggerLayout = "fill" >
< TabsList >
< TabsTrigger value = "recommendations" >추천</ TabsTrigger >
< TabsTrigger value = "subscriptions" >구독</ TabsTrigger >
</ TabsList >
</ TabsRoot >
< Box
ref = {scrollContainerRef}
data-testid = "scroll-auto-hide-container"
height = "360px"
overflowY = "auto"
>
< ScrollAutoHide
scrollContainerRef = {scrollContainerRef}
data-testid = "scroll-auto-hide-root"
asChild
>
< Flex
gap = "spacingX.betweenChips"
px = "spacingX.globalGutter"
py = "x4"
bg = "bg.layerDefault"
overflowX = "auto"
style = {{ zIndex: 1 }}
>
{ FILTERS . map (( filter ) => (
< Chip.Button key = {filter} size = "small" >
< Chip.Label >{filter}</ Chip.Label >
< Chip.SuffixIcon >
< Icon svg = {< IconChevronDownFill />} />
</ Chip.SuffixIcon >
</ Chip.Button >
))}
</ Flex >
</ ScrollAutoHide >
< VStack as = "ul" gap = "x5" px = "spacingX.globalGutter" py = "x4" >
{ ARTICLES . map (( article ) => (
< Box as = "li" key = {article.title}>
< VStack gap = "x2_5" >
< HStack gap = "x1_5" align = "center" >
< Avatar fallback = {< IdentityPlaceholder identity = "person" />} size = "20" />
< Text textStyle = "t4Medium" color = "fg.neutral" >
{article.author}
</ Text >
</ HStack >
< VStack gap = "x2" >
< VStack gap = "x1" >
< Text as = "h3" textStyle = "t5Bold" color = "fg.neutral" maxLines = { 1 }>
{article.title}
</ Text >
< Text as = "p" textStyle = "t4Regular" color = "fg.neutralMuted" maxLines = { 2 }>
{article.content}
</ Text >
</ VStack >
< HStack align = "center" gap = "x2" >
{article.popular && (
< Badge variant = "outline" tone = "brand" >
인기
</ Badge >
)}
< TagGroupRoot size = "t4" tone = "neutralSubtle" >
< TagGroupItem label = {article.category} />
< TagGroupItem label = "서초2동" />
< TagGroupItem label = {article.date} />
</ TagGroupRoot >
</ HStack >
</ VStack >
</ VStack >
</ Box >
))}
</ VStack >
</ Box >
</ Box >
);
}
npx @seed-design/cli@latest add breeze:scroll-auto-hide pnpm dlx @seed-design/cli@latest add breeze:scroll-auto-hide yarn dlx @seed-design/cli@latest add breeze:scroll-auto-hide bun x @seed-design/cli@latest add breeze:scroll-auto-hide
Breeze 문서