Time picker

사용자가 특정 시간 및 시간의 범위를 지정 및 선택할 수 있도록 도움을 주는 요소입니다.

Basic usage

사용자가 YYYY. MM. DD순으로 날짜를 입력할 수 있는 폼을 제공합니다.

  • locale, timezone을 지정할 수 있습니다.
  • 기본적으로 TextField 컴포넌트를 사용합니다.
  • 항상 유효한 Date 값을 가지고 있지는 않습니다. Invalid date 상태를 가질 수 있습니다.
import { TimePicker } from '@wanteddev/wds';

const Demo = () => {
  return (
    <TimePicker width="40%" />
  )
}

export default Demo;

Format

input에 시간이 표시되는 format 을 지정할 수 있습니다. 아래 format을 모두 사용할 수 있습니다.

  • H, HH, h, hh
  • m, mm
  • s, ss
  • A, a
  • YYYY, YY
  • M, MM, MMM, MMMM
  • D, DD

12시간 형식일 경우 a 또는 Ah 또는 hh를 함께 사용해야하며 24시간 형식일 경우 H 또는 HH만 사용합니다.

import { TimePicker, FlexBox } from '@wanteddev/wds';

const Demo = () => {
  return (
    <FlexBox flexDirection="column" alignItems="center" gap="20px" sx={{ width: '100%' }}>
      <TimePicker width="40%" format="a hh:mm:ss" />
      <TimePicker width="40%" format="a h:m:s" />
      <TimePicker width="40%" format="HH:mm:ss" />
      <TimePicker width="40%" format="H:m:s" />
      <TimePicker width="40%" format="mm:ss" />
      <TimePicker width="40%" format="m:s" />
    </FlexBox>
  )
}

export default Demo;

Localization

  • Locale
    • 기본적으로 locale은 ko-KR 으로 지정되어 있습니다.
    • Intl API 에서 지원하는 언어를 모두 import 없이 사용할 수 있습니다.
  • Timezone
    • 기본적으로 timezone은 사용자의 환경에 따라 결정됩니다.
    • dayjs 에서 지원하는 timezone과 UTC 를 모두 import 없이 사용할 수 있습니다.
import { TimePicker, FlexBox } from '@wanteddev/wds';

const Demo = () => {
  return (
    <FlexBox flexDirection="column" alignItems="center" gap="20px" sx={{ width: '100%' }}>
      <TimePicker width="40%" defaultValue={new Date()} locale="ko-KR" timezone="Asia/Seoul" />
      <TimePicker width="40%" defaultValue={new Date()} locale="en-US" timezone="UTC" />
    </FlexBox>
  )
}

export default Demo;

Views

views, defaultView, view, onViewChange 로 선택 UI를 조정할 수 있습니다.

'meridiem' | 'hour' | 'minute' | 'second' 를 지원합니다.

import { TimePicker, FlexBox } from '@wanteddev/wds';

const Demo = () => {

  return (
    <FlexBox flexDirection="column" alignItems="center" gap="20px" sx={{ width: '100%' }}>
      <TimePicker
        width="40%"
        format="mm:ss"
        views={['minute', 'second']}
      />
      <TimePicker
        width="40%"
        format="hh a"
        views={['hour', 'meridiem']}
      />
    </FlexBox>
  )
}

export default Demo;

ActionArea

PickerActionArea 를 사용하여 하단 영역의 버튼을 커스텀 할 수 있습니다.

총 4가지 variant를 사용할 수 있습니다.

  • now
  • accept
  • cancel
  • reset

Checkbox 컴포넌트와 함께 사용할 때에는 disableLastUnitClickClose prop 을 사용하여 더욱 자연스러운 동작을 구현할 수 있

import { TimePicker, FlexBox, PickerActionArea, PickerActionAreaButton, FormField, FormControl, FormLabel, Checkbox } from '@wanteddev/wds';

const Demo = () => {
  return (
    <FlexBox flexDirection="column" alignItems="center" gap="20px" sx={{ width: '100%' }}>
      <TimePicker
        width="40%"
        placeholder="현재, 확인"
        actionArea={
          <PickerActionArea>
            <PickerActionAreaButton variant="now">현재</PickerActionAreaButton>
            <PickerActionAreaButton variant="accept">확인</PickerActionAreaButton>
          </PickerActionArea>
        }
      />

      <TimePicker
        width="40%"
        placeholder="취소, 초기화"
        actionArea={
          <PickerActionArea>
            <PickerActionAreaButton variant="cancel">취소</PickerActionAreaButton>
            <PickerActionAreaButton variant="reset">초기화</PickerActionAreaButton>
          </PickerActionArea>
        }
      />


      <TimePicker
        width="40%"
        placeholder="checkbox, 확인"
        disableLastUnitClickClose
        actionArea={
          <PickerActionArea>
            <FormField
              gap="8px"
              flexDirection="row"
            >
              <FormControl>
                <Checkbox size="medium" />
              </FormControl>
              <FormLabel
                sx={{ padding: "1px 0px" }}
              >
                텍스트
              </FormLabel>
            </FormField>
            <PickerActionAreaButton variant="accept">확인</PickerActionAreaButton>
          </PickerActionArea>
        }
      />
    </FlexBox>
  )
}

export default Demo;

Validation

  • min, max prop 을 사용하여 최소값과 최대값을 설정할 수 있습니다.
  • 키보드 조작을 통해서는 범위를 벗어나는 값을 입력할 수 있습니다.
  • Invalid date 상태를 가질 수 있습니다.
import { TimePicker, FlexBox, FormErrorMessage, FormField, FormLabel, FormControl } from '@wanteddev/wds';
import { Controller, useForm } from 'react-hook-form';

const minTime = new Date(new Date().setHours(10, 0, 0));
const maxTime = new Date(new Date().setHours(17, 30, 0));

const Demo = () => {
  const form = useForm({
    defaultValues: {
      time: new Date(),
    },
    mode: 'onChange'
  });

  return (
    <FlexBox as="form" justifyContent="center" gap="20px" sx={{ width: '100%' }}>
      <Controller
        control={form.control}
        name="time"
        rules={{
          validate: (value) => {
            if (isNaN(new Date(value).getTime())) {
              return '유효한 날짜를 입력해주세요.';
            }

            if (new Date(value).getTime() < minTime.getTime()) {
              return '최소 시간은 10시 이상이어야 합니다.';
            }

            if (new Date(value).getTime() > maxTime.getTime()) {
              return '최대 시간은 17시 30분 이하이어야 합니다.';
            }
            
          return true;
          },
        }}
        render={({ field, formState }) => (
          <FormField sx={{ width: '40%' }}>
            <FormLabel>시간</FormLabel>

            <FormControl>
              <TimePicker
                width="100%"
                format="HH시 mm분"
                minTime={minTime}
                maxTime={maxTime}
                inputRef={field.ref}
                value={field.value}
                onChange={field.onChange}
                invalid={!!formState.errors.time}
              />
            </FormControl>

            <FormErrorMessage>{formState.errors.time?.message}</FormErrorMessage>
          </FormField>
        )}
      />
    </FlexBox>
  )
}

export default Demo;

Form field

Form과 관련된 컴포넌트와 함께 사용할 수 있습니다.

  • FormField
  • FormControl
  • FormLabel
  • FormMessage
  • FormErrorMessage

Helper Message

import { FlexBox, FormField, FormControl, FormLabel, FormMessage, FormErrorMessage, TimePicker } from '@wanteddev/wds';

const Demo = () => {
  return (
    <FlexBox justifyContent="center" sx={{ width: '100%' }}>
      <FormField sx={{ width: '40%' }}>
        <FormLabel required sx={{ padding: '1px 0px' }}>Label</FormLabel>
        <FormControl>
          <TimePicker width="100%" />
        </FormControl>
        <FormMessage>Helper Message</FormMessage>
      </FormField>
    </FlexBox>
  )
}

export default Demo;

Customize

TextField 컴포넌트 대신 커스텀 컴포넌트를 사용할 수 있습니다.

react 18.3 미만의 버전을 사용 중이라면, forwardRef 를 필수로 작성해주세요.

import { TimePicker, TimePickerFieldProps, Chip, FlexBox } from '@wanteddev/wds';
import { forwardRef, useState } from 'react';

const CustomChipField = forwardRef<HTMLDivElement, TimePickerFieldProps>(
  ({ value, onClick, inputRef, invalid, ...props }, ref) => {
    return (
      <Chip ref={ref} onClick={onClick}>
        {value || 'YYYY.MM.DD'}
      </Chip>
    )
  }
)

const CustomFieldField = forwardRef<HTMLDivElement, TimePickerFieldProps>(
  ({ trailingContent, inputRef, invalid, ...props }, ref) => {
    return (
      <FlexBox ref={ref} gap="10px" alignItems="center">
        <input ref={inputRef} {...props} />
        {trailingContent}
      </FlexBox>
    )
  }
)

const Demo = () => {
  const [open1, setOpen1] = useState(false);
  const [open2, setOpen2] = useState(false);

  return (
    <FlexBox flexDirection="column" gap="12px" alignItems="center" sx={{ width: '100%' }}>
      <TimePicker
        width="40%"
        input={CustomChipField}
        open={open1}
        onOpenChange={setOpen1}
        onClick={() => setOpen1(true)}
      />
      <TimePicker
        width="40%"
        input={CustomFieldField}
        open={open2}
        onOpenChange={setOpen2}
      />
    </FlexBox>
  )
}

export default Demo;

Controlled

기본적으로 비제어 컴포넌트로 동작합니다.

value, onChange prop 을 사용하면 제어 컴포넌트로 동작합니다.

제어 컴포넌트와 비제어 컴포넌트는 React 공식 문서 를 참조해주세요.

import { TimePicker, FlexBox } from '@wanteddev/wds';
import { useState } from 'react';

const Demo = () => {
  const [value, setValue] = useState(new Date());

  return (
    <FlexBox alignItems="center" flexDirection="column" gap="12px" alignItems="center" sx={{ width: '100%' }}>
      <TimePicker defaultValue={new Date()} width="40%" />
      <TimePicker value={value} onChange={setValue} width="40%" />
    </FlexBox>
  )
}

export default Demo;

Accessibility

WAI-ARIA Datepicker dialog 패턴 대부분을 지원합니다.

  • Form Field 와 함께 사용할 때 더욱 명확한 접근성 속성을 주입할 수 있습니다.

API

TimePicker

NameTypesdefaultValue
open
boolean
-
defaultOpen
boolean
-
onOpenChange
(state: boolean) => void
-
format
string
"a hh:mm"
input
"symbol" | "object" | "div" | "a" | "abbr" | "address" | "area" | "article" | "aside" | "audio" | "b" | "base" | "bdi" | "bdo" | "big" | "blockquote" | "body" | "br" | "button" | "canvas" | "caption" | "center" | "cite" | "code" | "col" | "colgroup" | "data" | "datalist" | "dd" | "del" | "details" | "dfn" | "dialog" | "dl" | "dt" | "em" | "embed" | "fieldset" | "figcaption" | "figure" | "footer" | "form" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "head" | "header" | "hgroup" | "hr" | "html" | "i" | "iframe" | "img" | "input" | "ins" | "kbd" | "keygen" | "label" | "legend" | "li" | "link" | "main" | "map" | "mark" | "menu" | "menuitem" | "meta" | "meter" | "nav" | "noindex" | "noscript" | "ol" | "optgroup" | "option" | "output" | "p" | "param" | "picture" | "pre" | "progress" | "q" | "rp" | "rt" | "ruby" | "s" | "samp" | "search" | "slot" | "script" | "section" | "select" | "small" | "source" | "span" | "strong" | "style" | "sub" | "summary" | "sup" | "table" | "template" | "tbody" | "td" | "textarea" | "tfoot" | "th" | "thead" | "time" | "title" | "tr" | "track" | "u" | "ul" | "var" | "video" | "wbr" | "webview" | "svg" | "animate" | "animateMotion" | "animateTransform" | "circle" | "clipPath" | "defs" | "desc" | "ellipse" | "feBlend" | "feColorMatrix" | "feComponentTransfer" | "feComposite" | "feConvolveMatrix" | "feDiffuseLighting" | "feDisplacementMap" | "feDistantLight" | "feDropShadow" | "feFlood" | "feFuncA" | "feFuncB" | "feFuncG" | "feFuncR" | "feGaussianBlur" | "feImage" | "feMerge" | "feMergeNode" | "feMorphology" | "feOffset" | "fePointLight" | "feSpecularLighting" | "feSpotLight" | "feTile" | "feTurbulence" | "filter" | "foreignObject" | "g" | "image" | "line" | "linearGradient" | "marker" | "mask" | "metadata" | "mpath" | "path" | "pattern" | "polygon" | "polyline" | "radialGradient" | "rect" | "set" | "stop" | "switch" | "text" | "textPath" | "tspan" | "use" | "view" | { propTypes?: any; contextType?: React.Context<any>; defaultProps?: Partial<any>; displayName?: string; getDerivedStateFromProps?: React.GetDerivedStateFromProps<any, any>; getDerivedStateFromError?: React.GetDerivedStateFromError<any, any> } | (props: any) => null | string | number | bigint | false | true | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | Promise<React.ReactNode>
-
inputRef
null | (instance: null | HTMLInputElement) => void | () => VoidOrUndefinedOnly | RefObject
-
contentProps
{ offset?: number | undefined; position?: "top-start" | "top-center" | "top-end" | "right-start" | "right-center" | "right-end" | "bottom-start" | "bottom-center" | "bottom-end" | "left-start" | "left-center" | "left-end" | undefined; referenceHidden?: boolean | undefined; referenceHiddenOffsets?: SideObject | undefined; wrapperProps?: DefaultComponentProps<{}, "div"> | undefined; setContext?: ((context: { x: number; y: number; placement: Placement; strategy: Strategy; middlewareData: MiddlewareData; isPositioned: boolean; update: () => void; floatingStyles: React.CSSProperties; open: boolean; onOpenChange: (open: boolean, event?: Event | undefined, reason?: OpenChangeReason | undefined) => void; events: FloatingEvents; dataRef: React.MutableRefObject<ContextData>; nodeId: string | undefined; floatingId: string | undefined; refs: ExtendedRefs<ReferenceType>; elements: ExtendedElements<ReferenceType>; }) => void) | undefined; container?: Element | DocumentFragment | null | undefined; disablePortal?: boolean | undefined; children?: React.ReactNode } & { disableFocusScope?: boolean | undefined; loop?: boolean | undefined; trapped?: boolean | undefined; trappedContent?: boolean | undefined; onMountAutoFocus?: ((event: Event) => void) | undefined; onUnmountAutoFocus?: ((event: Event) => void) | undefined } & { sx?: SxProp }
-
onChange
(date: null | string | Date) => void
-
actionArea
ReactNode
-
disableLastUnitClickClose
boolean
-
defaultValue
null | string | Date
-
children
ReactNode
-
onReset
(prevValue: string) => void
-
disabled
boolean
-
sx
SxProp
-
xl
Merge<Pick<TextFieldDefaultProps, "height" | "width">, { sx?: CSSInterpolation; }> | undefined
-
lg
Merge<Pick<TextFieldDefaultProps, "height" | "width">, { sx?: CSSInterpolation; }> | undefined
-
md
Merge<Pick<TextFieldDefaultProps, "height" | "width">, { sx?: CSSInterpolation; }> | undefined
-
sm
Merge<Pick<TextFieldDefaultProps, "height" | "width">, { sx?: CSSInterpolation; }> | undefined
-
xs
Merge<Pick<TextFieldDefaultProps, "height" | "width">, { sx?: CSSInterpolation; }> | undefined
-
leadingContent
ReactNode
-
trailingContent
ReactNode
-
value
null | string | Date
-
positive
boolean
-
height
Property.Height<string | number> | undefined
-
width
Property.Width<string | number> | undefined
-
invalid
boolean
-
views
Array<"meridiem" | "hour" | "minute" | "second">
-
locale
string
"ko-KR"
timezone
string
-
onChangeComplete
(value: null | string | Date) => void
-
readOnly
boolean
-
trailingButton
ReactNode
-
minTime
null | string | Date
-
maxTime
null | string | Date
-

PickerActionArea

NameTypesdefaultValue
children
ReactNode
-
variant
"strong" | "neutral" | "compact" | "cancel"
-
extra
boolean
-
caption
ReactNode
-
extraContent
ReactNode
-
compactContent
ReactNode
-
background
boolean
-
divider
boolean
-
sx
SxProp
-

PickerActionAreaButton

NameTypesdefaultValue
as
ElementType
-
variant
"cancel" | "accept" | "reset" | "now"
-
color
"primary" | "assistive"
-
children
ReactNode
-
disabled
boolean
-
sx
SxProp
-
xl
Merge<Pick<TextButtonDefaultProps, "size">, { sx?: CSSInterpolation; }> | undefined
-
lg
Merge<Pick<TextButtonDefaultProps, "size">, { sx?: CSSInterpolation; }> | undefined
-
md
Merge<Pick<TextButtonDefaultProps, "size">, { sx?: CSSInterpolation; }> | undefined
-
sm
Merge<Pick<TextButtonDefaultProps, "size">, { sx?: CSSInterpolation; }> | undefined
-
xs
Merge<Pick<TextButtonDefaultProps, "size">, { sx?: CSSInterpolation; }> | undefined
-
disableInteraction
boolean
-
leadingContent
ReactNode
-
trailingContent
ReactNode
-
loading
boolean
-
disableLoadingPreventEvents
boolean
-
size
"small" | "medium"
-

© 2026 Wanted Lab, Inc.