component — date & time
Date Picker
A segmented date input paired with a calendar overlay, built on react-aria-components and styled with the quebi design system. The trigger uses the quebi input chrome with a calendar-icon button; a popover (or modal on mobile) holds the calendar. The trigger's segments format like DateField's — pass shouldForceLeadingZeros for two-digit day and month.
- form
- input
- date
- datetime
- calendar
- popover
- interactive
- leading-zeros
06 examples
Default
A labelled date picker with a calendar overlay.
With value
Pre-filled via @internationalized/date.
Two digits
shouldForceLeadingZeros pads the day and month segments to two digits (30.06.2026 rather than the 30.6.2026 de-DE asks for). The year is unaffected, and the calendar overlay is unchanged.
Invalid
Validation state surfaces a red border and an error message.
Disabled
Controlled
source
date-picker.tsxCopy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.
"use client"
import type { DateDuration } from "@internationalized/date"
import { CalendarDays } from "lucide-react"
import { useEffect, useState } from "react"
import type {
DatePickerProps as DatePickerPrimitiveProps,
DateValue,
GroupProps,
PopoverProps,
} from "react-aria-components"
import {
Button,
composeRenderProps,
DatePicker as DatePickerPrimitive,
Group,
} from "react-aria-components"
import { Calendar, type CalendarHeaderVariant } from "@/components/calendar"
import { DateInput } from "@/components/date-field"
import { ModalContent } from "@/components/modal"
import { PopoverContent } from "@/components/popover"
import { RangeCalendar } from "@/components/range-calendar"
import { cn } from "@/lib/utils"
import { fieldStyles } from "@/components/field"
/**
* Date Picker — quebi design system
*
* A segmented date input paired with a calendar overlay. The trigger uses the
* box of `Input`, with a calendar-icon button on the right; clicking it opens a Popover
* holding the Calendar (or a Modal on mobile). Composes
* @/components/{calendar,range-calendar,modal,popover,date-field}. Foundational
* — the Conform date-picker / date-range-picker variants depend on it.
*
* The trigger's segments are a `DateInput`, so they format the way `DateField`
* does: locale-derived, `30.6.2026` under `de-DE`. `shouldForceLeadingZeros`
* pads the day and month to two digits (`30.06.2026`); the calendar overlay is
* unaffected either way.
*/
const MOBILE_BREAKPOINT = 768
/** Inlined use-mobile hook: tracks whether the viewport is below md. */
const useIsMobile = () => {
const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined)
useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return isMobile
}
export interface DatePickerProps<T extends DateValue> extends DatePickerPrimitiveProps<T> {
popover?: Omit<PopoverProps, "children">
}
export function DatePicker<T extends DateValue>({
className,
children,
popover,
...props
}: DatePickerProps<T>) {
return (
<DatePickerPrimitive
data-slot="control"
className={composeRenderProps(className, (resolved) =>
cn("group", fieldStyles, resolved),
)}
{...props}
>
{(values) => (
<>
{typeof children === "function" ? children(values) : children}
<DatePickerOverlay {...popover} />
</>
)}
</DatePickerPrimitive>
)
}
export interface DatePickerOverlayProps extends Omit<PopoverProps, "children"> {
range?: boolean
visibleDuration?: DateDuration
pageBehavior?: "visible" | "single"
/** Calendar header treatment — the in-body month picker (default) or chevron steppers. */
variant?: CalendarHeaderVariant
}
export function DatePickerOverlay({
visibleDuration = { months: 1 },
pageBehavior = "visible",
placement = "bottom",
range,
variant,
...props
}: DatePickerOverlayProps) {
const isMobile = useIsMobile()
return isMobile ? (
<ModalContent aria-label="Date picker" closeButton={false}>
<div className="flex justify-center p-6">
{range ? (
<RangeCalendar
pageBehavior={pageBehavior}
visibleDuration={visibleDuration}
variant={variant}
/>
) : (
<Calendar variant={variant} />
)}
</div>
</ModalContent>
) : (
<PopoverContent
placement={placement}
arrow={false}
// min/max sized for a 7×36px grid plus gutter — the calendar fits without
// overflow.
className={cn(
"flex justify-center p-3",
visibleDuration?.months === 1 ? "w-[304px] max-w-[304px]" : "max-w-none",
)}
{...props}
>
{range ? (
<RangeCalendar
pageBehavior={pageBehavior}
visibleDuration={visibleDuration}
variant={variant}
/>
) : (
<Calendar variant={variant} />
)}
</PopoverContent>
)
}
/**
* DatePickerTrigger — quebi design system
*
* DateInput on the left + calendar-icon button on the right, styled as one
* control: one box around both, owned by the wrapper. The inner
* DateInput is rendered `bare` so it draws no edge of its own.
*/
export function DatePickerTrigger({ className, ...props }: GroupProps) {
return (
<Group
data-slot="control"
className={composeRenderProps(className, (resolved) =>
cn(
// `Input`'s box, around the segments and the calendar button alike;
// focus anywhere inside, or the open calendar, turns its edge.
"group/dpt flex w-full items-stretch overflow-hidden",
"quebi-field",
"transition-[border-color,box-shadow] duration-150",
"focus-within:shadow-(--q-field-focus) group-open:shadow-(--q-field-focus)",
"data-invalid:[--q-field-edge:var(--q-danger)] data-invalid:focus-within:shadow-(--q-field-focus-danger)",
resolved,
),
)}
{...props}
>
<div className="flex-1">
<DateInput bare />
</div>
<Button
data-slot="date-picker-trigger"
className={cn(
"inline-flex cursor-pointer items-center bg-transparent ps-3 pe-(--q-field-px) text-quebi-fg-subtle",
"transition-colors duration-150 hover:text-quebi-fg",
"outline-none focus-visible:text-quebi-fg focus-visible:ring-2 focus-visible:ring-quebi-focus focus-visible:ring-inset",
)}
>
<CalendarDays data-slot="icon" className="size-4" />
</Button>
</Group>
)
}