All components
Date Picker
Date input with calendar trigger, month navigation, and a selectable day grid.
Delivery date
March 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
12345678910111213141516171819import { DatePicker } from "#components/ui/base-ui/date-picker.tsx";
export function Example() { return ( // Base UI has no date-picker machine, so Root owns the selected date and // visible month (value/defaultValue/onValueChange); Content renders the // month navigation and day grid, and parts compose through render <DatePicker.Root defaultValue={new Date(2026, 2, 18)} size="md"> <DatePicker.Label>Delivery date</DatePicker.Label> <DatePicker.Control> <DatePicker.Input aria-label="Delivery date" /> <DatePicker.IndicatorGroup> <DatePicker.Trigger /> </DatePicker.IndicatorGroup> </DatePicker.Control> <DatePicker.Content /> </DatePicker.Root> );}Sizes
Delivery date
xsMarch 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
Delivery date
smMarch 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
Delivery date
mdMarch 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
Delivery date
lgMarch 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
Delivery date
xlMarch 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
Install
1npx stylexblocks add date-pickerSource
components/ui/base-ui/date-picker.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973import * as stylex from "@stylexjs/stylex";import { createContext, useContext, useMemo, useState } from "react";import { mergeProps } from "@base-ui-components/react/merge-props";import { useRender } from "@base-ui-components/react/use-render";import { semanticShadows } from "@stylexblocks/ui/semantic-tokens/shadows.stylex.ts";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles, type SlotRecipeVariants,} from "#lib/recipe-props.ts";import { datePickerTriggerIconVars, datePickerPrevTriggerIconVars, datePickerNextTriggerIconVars, datePickerClearTriggerIconVars,} from '@stylexblocks/ui/slot-recipes/date-picker.stylex.ts'
import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'import { zIndices } from '@stylexblocks/ui/tokens/z-indices.stylex.ts'
const datePickerRoot = stylex.create({ base: { display: 'flex', flexDirection: 'column', gap: spacing._1_5, width: sizes.full, '--datepicker-indicators-offset': sizes._3, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, },})
const datePickerRootSizes = stylex.create({ xs: { '--datepicker-input-height': sizes._6, '--datepicker-input-px': sizes._2, '--datepicker-indicators-offset': sizes._2, }, sm: { '--datepicker-input-height': sizes._7, '--datepicker-input-px': sizes._2_5, '--datepicker-indicators-offset': sizes._2_5, }, md: { '--datepicker-input-height': sizes._8, '--datepicker-input-px': sizes._3, }, lg: { '--datepicker-input-height': sizes._10, '--datepicker-input-px': sizes._4_5, }, xl: { '--datepicker-input-height': sizes._12, '--datepicker-input-px': sizes._6, },})
const datePickerLabel = stylex.create({ base: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, fontWeight: fontWeights.medium, },})
const datePickerIndicatorGroup = stylex.create({ base: { position: 'absolute', insetInlineEnd: 'var(--datepicker-indicators-offset)', top: '50%', transform: 'translateY(-50%)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: spacing._1, },})
const datePickerIndicatorGroupVariants = stylex.create({ flushed: { insetInlineEnd: 0, },})
const datePickerControl = stylex.create({ base: { display: 'flex', alignItems: 'center', gap: spacing._2, width: sizes.full, position: 'relative', },})
const datePickerInput = stylex.create({ base: { flex: 1, minWidth: 0, height: 'var(--datepicker-input-height)', '--input-height': 'var(--datepicker-input-height)', paddingInline: 'var(--datepicker-input-px)', paddingInlineEnd: `calc(var(--datepicker-indicators-offset) + ${sizes._6})`, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, backgroundColor: 'transparent', borderRadius: semanticRadii.controlMd, outline: 'none', appearance: 'none', color: semanticColors.fg, '--focus-color': colorPalette.focusRing, '--error-color': semanticColors.borderError, '--focus-ring-color': { default: null, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, borderColor: { default: null, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, },})
const datePickerInputVariants = stylex.create({ outline: { backgroundColor: 'transparent', borderWidth: '1px', borderColor: { default: semanticColors.border, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, '--focus-ring-color': { default: 'var(--focus-color)', ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, }, subtle: { borderWidth: '1px', borderColor: { default: 'transparent', ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, backgroundColor: semanticColors.bgMuted, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, '--focus-ring-color': { default: 'var(--focus-color)', ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, }, flushed: { backgroundColor: 'transparent', borderBottomWidth: '1px', borderBottomColor: semanticColors.border, borderRadius: 0, paddingInline: 0, borderColor: { default: null, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', ':focus-visible': { default: 'var(--focus-color)', ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, }, boxShadow: { default: null, ':focus-visible': { default: '0px 1px 0px 0px var(--focus-color)', ':is([data-invalid], [aria-invalid="true"])': '0px 1px 0px 0px var(--error-color)', }, }, },})
const datePickerTrigger = stylex.create({ base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: sizes._6, height: sizes._6, borderRadius: semanticRadii.controlSm, color: { default: semanticColors.fgMuted, ':hover': semanticColors.fg, }, outline: 'none', outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, focusRingWidth: '2px', [datePickerTriggerIconVars.width]: sizes._4, [datePickerTriggerIconVars.height]: sizes._4, },})
const datePickerTriggerIcon = stylex.create({ base: { width: datePickerTriggerIconVars.width, height: datePickerTriggerIconVars.height, },})
const datePickerContent = stylex.create({ base: { display: 'flex', flexDirection: 'column', gap: spacing._3, padding: spacing._3, minWidth: '18rem', color: semanticColors.fg, '--date-picker-z-index': zIndices.layer3, zIndex: 'calc(var(--date-picker-z-index) + var(--layer-index, 0))', outline: 'none', },})
const datePickerView = stylex.create({ base: { display: 'flex', flexDirection: 'column', gap: spacing._3, },})
const datePickerViewSizes = stylex.create({ xs: { '--table-cell-size': sizes._8, '--datepicker-nav-trigger-size': sizes._7, '--datepicker-select-height': sizes._8, }, sm: { '--table-cell-size': sizes._9, '--datepicker-nav-trigger-size': sizes._8, '--datepicker-select-height': sizes._9, }, md: { '--table-cell-size': sizes._10, '--datepicker-nav-trigger-size': sizes._8, '--datepicker-select-height': sizes._10, }, lg: { '--table-cell-size': sizes._10, '--datepicker-nav-trigger-size': sizes._9, '--datepicker-select-height': sizes._10, }, xl: { '--table-cell-size': sizes._10, '--datepicker-nav-trigger-size': sizes._9, '--datepicker-select-height': sizes._10, },})
const datePickerViewControl = stylex.create({ base: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: spacing._2, height: 'var(--datepicker-nav-trigger-size)', },})
const datePickerViewTrigger = stylex.create({ base: { display: 'inline-flex', flex: 1, alignItems: 'center', justifyContent: 'center', gap: spacing._1, paddingBlock: spacing._1_5, paddingInline: spacing._2, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, fontWeight: fontWeights.semibold, borderRadius: semanticRadii.controlMd, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, focusRingWidth: '2px', backgroundColor: { default: null, ':hover': colorPalette.subtle, }, },})
const datePickerPrevTrigger = stylex.create({ base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 'var(--datepicker-nav-trigger-size)', height: 'var(--datepicker-nav-trigger-size)', borderRadius: semanticRadii.controlMd, color: semanticColors.fg, boxShadow: { default: null, ':focus-visible': '0 0 0 2px var(--colors-color-palette-focus-ring)', }, focusRingWidth: '2px', backgroundColor: { default: null, ':hover': colorPalette.subtle, }, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, [datePickerPrevTriggerIconVars.width]: sizes._4, [datePickerPrevTriggerIconVars.height]: sizes._4, },})
const datePickerPrevTriggerIcon = stylex.create({ base: { width: datePickerPrevTriggerIconVars.width, height: datePickerPrevTriggerIconVars.height, },})
const datePickerNextTrigger = stylex.create({ base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 'var(--datepicker-nav-trigger-size)', height: 'var(--datepicker-nav-trigger-size)', borderRadius: semanticRadii.controlMd, color: semanticColors.fg, boxShadow: { default: null, ':focus-visible': '0 0 0 2px var(--colors-color-palette-focus-ring)', }, focusRingWidth: '2px', backgroundColor: { default: null, ':hover': colorPalette.subtle, }, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, [datePickerNextTriggerIconVars.width]: sizes._4, [datePickerNextTriggerIconVars.height]: sizes._4, },})
const datePickerNextTriggerIcon = stylex.create({ base: { width: datePickerNextTriggerIconVars.width, height: datePickerNextTriggerIconVars.height, },})
const datePickerRangeText = stylex.create({ base: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, fontWeight: fontWeights.semibold, },})
const datePickerTable = stylex.create({ base: { borderCollapse: 'separate', borderSpacing: 0, },})
const datePickerTableHeader = stylex.create({ base: { width: 'var(--table-cell-size)', paddingBlock: spacing._2, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, fontWeight: fontWeights.medium, color: semanticColors.fgMuted, textAlign: 'center', textTransform: 'uppercase', },})
const datePickerTableCell = stylex.create({ base: { paddingBlock: spacing._0_5, },})
const datePickerTableCellTrigger = stylex.create({ base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', minWidth: 'var(--table-cell-size)', minHeight: 'var(--table-cell-size)', fontSize: fontSizes.sm, lineHeight: lineHeights.sm, borderRadius: semanticRadii.controlMd, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, focusRingWidth: '2px', focusRingOffset: '0px', cursor: 'default', position: 'relative', backgroundColor: { default: null, ':hover': colorPalette.subtle, }, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.4, }, },})
const datePickerTableCellTriggerHideOutsideDays = stylex.create({ true: {},})
const datePickerMonthSelect = stylex.create({ base: { height: 'var(--datepicker-select-height)', paddingInlineStart: spacing._2, paddingInlineEnd: spacing._8, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, borderWidth: '1px', borderRadius: semanticRadii.controlMd, outline: 'none', outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, appearance: 'none', fieldSizing: 'content', backgroundImage: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E\")", backgroundRepeat: 'no-repeat', backgroundPosition: 'right 0.375rem center', backgroundSize: '1.25em', },})
const datePickerYearSelect = stylex.create({ base: { height: 'var(--datepicker-select-height)', paddingInlineStart: spacing._2, paddingInlineEnd: spacing._8, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, borderWidth: '1px', borderRadius: semanticRadii.controlMd, outline: 'none', outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, appearance: 'none', fieldSizing: 'content', backgroundImage: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E\")", backgroundRepeat: 'no-repeat', backgroundPosition: 'right 0.375rem center', backgroundSize: '1.25em', },})
const datePickerClearTrigger = stylex.create({ base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: sizes._6, height: sizes._6, flexShrink: 0, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, borderRadius: semanticRadii.controlMd, color: { default: semanticColors.fgMuted, ':hover': semanticColors.fg, }, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, [datePickerClearTriggerIconVars.width]: sizes._4, [datePickerClearTriggerIconVars.height]: sizes._4, },})
const datePickerClearTriggerIcon = stylex.create({ base: { width: datePickerClearTriggerIconVars.width, height: datePickerClearTriggerIconVars.height, },})
export type DatePickerSize = keyof typeof datePickerRootSizes
export type DatePickerHideOutsideDays = keyof typeof datePickerTableCellTriggerHideOutsideDays
export type DatePickerVariant = keyof typeof datePickerIndicatorGroupVariants
const datePickerRecipe = defineSlotRecipe({ slots: { root: { styles: datePickerRoot, sizes: datePickerRootSizes }, label: { styles: datePickerLabel }, indicatorGroup: { styles: datePickerIndicatorGroup, variants: datePickerIndicatorGroupVariants, }, control: { styles: datePickerControl }, input: { styles: datePickerInput, variants: datePickerInputVariants }, trigger: { styles: datePickerTrigger }, content: { styles: datePickerContent }, view: { styles: datePickerView, sizes: datePickerViewSizes }, viewControl: { styles: datePickerViewControl }, viewTrigger: { styles: datePickerViewTrigger }, prevTrigger: { styles: datePickerPrevTrigger }, nextTrigger: { styles: datePickerNextTrigger }, rangeText: { styles: datePickerRangeText }, table: { styles: datePickerTable }, tableHeader: { styles: datePickerTableHeader }, tableCell: { styles: datePickerTableCell }, tableCellTrigger: { styles: datePickerTableCellTrigger }, monthSelect: { styles: datePickerMonthSelect }, yearSelect: { styles: datePickerYearSelect }, clearTrigger: { styles: datePickerClearTrigger }, }, defaultVariants: { size: "md", variant: "outline" },});
const chrome = stylex.create({ btnReset: { appearance: "none", backgroundColor: "transparent", borderWidth: 0, borderStyle: "none", padding: 0, margin: 0, fontFamily: "inherit", fontSize: "inherit", color: "inherit", cursor: "pointer", textAlign: "inherit", }, panel: { backgroundColor: semanticColors.bgPanel, borderRadius: semanticRadii.panel, boxShadow: semanticShadows.overlay, }, dayOutside: { color: semanticColors.fgMuted, opacity: 0.5 }, daySelected: { backgroundColor: { default: semanticColors.indigoSolid, ":hover": semanticColors.indigoSolid, }, color: semanticColors.indigoContrast, },});
function CalendarIcon(props: React.ComponentProps<"svg">) { return ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}> <rect x="2.5" y="3.5" width="11" height="10" rx="1.5" stroke="currentColor" strokeWidth="1.5" /> <path d="M2.5 6.5h11M5.5 2v2.5M10.5 2v2.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> </svg> );}
function ChevronLeftIcon(props: React.ComponentProps<"svg">) { return ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}> <path d="M10 4 6 8l4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> );}
function ChevronRightIcon(props: React.ComponentProps<"svg">) { return ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}> <path d="m6 4 4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> );}
const WEEK_DAYS = ["S", "M", "T", "W", "T", "F", "S"];
function sameDay(a: Date | null, b: Date) { return ( a != null && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() );}
/** Weeks (Sunday-first) covering the given month, padded with outside days. */function monthGrid(month: Date): Date[][] { const first = new Date(month.getFullYear(), month.getMonth(), 1); const cursor = new Date(first); cursor.setDate(1 - first.getDay()); const weeks: Date[][] = []; do { const week: Date[] = []; for (let i = 0; i < 7; i++) { week.push(new Date(cursor)); cursor.setDate(cursor.getDate() + 1); } weeks.push(week); } while (cursor.getMonth() === month.getMonth()); return weeks;}
interface DatePickerContextValue { hideOutsideDays: boolean; value: Date | null; select: (date: Date) => void; month: Date; setMonth: (month: Date) => void;}
const DatePickerContext = createContext<DatePickerContextValue>({ hideOutsideDays: true, value: null, select: () => {}, month: new Date(2026, 0, 1), setMonth: () => {},});
const { withProvider, withContext, VariantContext } = createSlotRecipeContext(datePickerRecipe);
export type DatePickerVariantProps = Partial<SlotRecipeVariants<typeof datePickerRecipe>> & { hideOutsideDays?: boolean;};
function RootNode({ render, hideOutsideDays = true, value, defaultValue = null, onValueChange, ...rest}: useRender.ComponentProps<"div"> & { hideOutsideDays?: boolean; value?: Date | null; defaultValue?: Date | null; onValueChange?: (value: Date) => void;}) { const [inner, setInner] = useState<Date | null>(defaultValue); const current = value !== undefined ? value : inner; const [month, setMonth] = useState<Date>(() => { const base = current ?? new Date(); return new Date(base.getFullYear(), base.getMonth(), 1); }); const ctx = useMemo<DatePickerContextValue>( () => ({ hideOutsideDays, value: current, select: (date: Date) => { setInner(date); onValueChange?.(date); }, month, setMonth, }), [hideOutsideDays, current, month, onValueChange], ); const element = useRender({ defaultTagName: "div", render: render as useRender.RenderProp, props: rest, }); return <DatePickerContext.Provider value={ctx}>{element}</DatePickerContext.Provider>;}
function InputNode({ render, ...rest}: Omit<useRender.ComponentProps<"input">, "value">) { const ctx = useContext(DatePickerContext); const formatted = ctx.value ? `${String(ctx.value.getMonth() + 1).padStart(2, "0")}/${String(ctx.value.getDate()).padStart(2, "0")}/${ctx.value.getFullYear()}` : ""; return useRender({ defaultTagName: "input", render: render as useRender.RenderProp, props: mergeProps( { readOnly: true, value: formatted, placeholder: "mm/dd/yyyy", }, rest, ), });}
function TriggerNode({ render, children, ...rest}: useRender.ComponentProps<"button">) { return useRender({ defaultTagName: "button", render: render as useRender.RenderProp, props: mergeProps( { type: "button" as const, "aria-label": "Open calendar", children: children ?? <CalendarIcon {...stylex.props(datePickerTriggerIcon.base)} />, }, rest, ), });}
const View = withContext("div", "view", "DatePickerView");const ViewControl = withContext("div", "viewControl", "DatePickerViewControl");const ViewTrigger = withContext("span", "viewTrigger", "DatePickerViewTrigger");const RangeText = withContext("span", "rangeText", "DatePickerRangeText");const Table = withContext("table", "table", "DatePickerTable");const TableHeader = withContext("th", "tableHeader", "DatePickerTableHeader");const TableCell = withContext("td", "tableCell", "DatePickerTableCell");
function DefaultCalendar() { const ctx = useContext(DatePickerContext); const v = useContext(VariantContext); const weeks = useMemo(() => monthGrid(ctx.month), [ctx.month]); const monthLabel = ctx.month.toLocaleDateString("en-US", { month: "long", year: "numeric", }); const shift = (delta: number) => ctx.setMonth(new Date(ctx.month.getFullYear(), ctx.month.getMonth() + delta, 1)); return ( <View> <ViewControl> <button type="button" aria-label="Previous month" onClick={() => shift(-1)} {...stylex.props( chrome.btnReset, ...resolveSlotStyles(datePickerRecipe.slots.prevTrigger, v), )} > <ChevronLeftIcon {...stylex.props(datePickerPrevTriggerIcon.base)} /> </button> <ViewTrigger> <RangeText>{monthLabel}</RangeText> </ViewTrigger> <button type="button" aria-label="Next month" onClick={() => shift(1)} {...stylex.props( chrome.btnReset, ...resolveSlotStyles(datePickerRecipe.slots.nextTrigger, v), )} > <ChevronRightIcon {...stylex.props(datePickerNextTriggerIcon.base)} /> </button> </ViewControl> <Table> <thead> <tr> {WEEK_DAYS.map((weekDay, i) => ( <TableHeader key={i} scope="col"> {weekDay} </TableHeader> ))} </tr> </thead> <tbody> {weeks.map((week, wi) => ( <tr key={wi}> {week.map((day, di) => { const outside = day.getMonth() !== ctx.month.getMonth(); const selected = !outside && sameDay(ctx.value, day); return ( <TableCell key={di}> <button type="button" aria-pressed={selected} data-outside-range={outside ? "" : undefined} onClick={() => { if (!outside) ctx.select(day); }} {...stylex.props( chrome.btnReset, ...resolveSlotStyles(datePickerRecipe.slots.tableCellTrigger, v), outside && chrome.dayOutside, outside && ctx.hideOutsideDays && datePickerTableCellTriggerHideOutsideDays.true, selected && chrome.daySelected, )} > {day.getDate()} </button> </TableCell> ); })} </tr> ))} </tbody> </Table> </View> );}
function ContentNode({ render, children, ...rest}: useRender.ComponentProps<"div">) { return useRender({ defaultTagName: "div", render: render as useRender.RenderProp, props: mergeProps({ children: children ?? <DefaultCalendar /> }, rest), });}
/** * DatePicker — date input with an inline calendar. Base UI ships no * date-picker machine, so Root owns the selected date and visible month * (controlled via `value`/`onValueChange` or uncontrolled via * `defaultValue`). `<DatePicker.Root variant size>` resolves the recipe * once; every part inherits it through context. Content renders the month * navigation and day grid by default. Composition per part via `render`. */export const DatePicker = { Root: withProvider(RootNode, "root", "DatePickerRoot"), Label: withContext("span", "label", "DatePickerLabel"), Control: withContext("div", "control", "DatePickerControl"), Input: withContext(InputNode, "input", "DatePickerInput"), IndicatorGroup: withContext("div", "indicatorGroup", "DatePickerIndicatorGroup"), Trigger: withContext( TriggerNode, (v) => [chrome.btnReset, ...resolveSlotStyles(datePickerRecipe.slots.trigger, v)], "DatePickerTrigger", ), Content: withContext( ContentNode, (v) => [...resolveSlotStyles(datePickerRecipe.slots.content, v), chrome.panel], "DatePickerContent", ),};
export type DatePickerRootProps = React.ComponentProps<typeof DatePicker.Root>;