Skip to content
garn

DatePicker

A date field you can both type into and pick from a calendar — format-driven, with a native Date value. Ships a DateRangePicker sibling for ranges.

Stablev0.4.2added in v0.2.0@garn/ui/date-picker
On this page

Default

A default date field (type or pick).

Format

A custom `format` (dd.MM.yyyy) driving display + parse.

Constraints

minDate + isDateUnavailable (weekdays this year).

Validation

A required field that clears its error once set.

Presets

Quick-pick shortcuts beside the calendar.

States

Disabled and invalid.

Range

DateRangePicker — a start/end range.

Range Presets

DateRangePicker with preset ranges + dual-month.

Installation

garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.

terminal
npx garn-ui add date-picker
First time? Set up garn in your project
Registry
date-picker
Deps
date-fnsreact-day-pickerclass-variance-authoritylucide-react
Registry deps
utilsdate-iofield-surfaceuse-controllable-stateuse-merged-refcalendarpopoverbutton

Import

import { DateRangePicker } from "@garn/ui/date-picker";

Anatomy

The parts this component is built from — compose them to assemble it.

DateRangePicker[data-slot="date-range-picker"]

The range sibling — value is { start, end }; renders a dual-month Calendar (mode=range) with optional presets and hidden <name>-start/<name>-end form inputs (each a yyyy-MM-dd calendar date).

Props

The component's public props and their types.

appearance
Description
No description.
Type
"ghost" | "outline" | "soft"
Default
outline
size
Description
No description.
Type
"lg" | "md" | "sm" | "xl" | "xs"
Default
md
"aria-invalid"
Description
Marks the field invalid — colors the surface and sets `aria-invalid` on the input.
Type
"false" | "true" | boolean
"aria-label"
Description
Accessible name for the field when there is no visible label.
Type
string
"aria-labelledby"
Description
Id(s) of element(s) labelling the field.
Type
string
className
Description
Extra classes for the field surface (the bordered wrapper).
Type
string
clearable
Description
Whether to show a clear ✕ when a date is set.
Type
boolean
Default
true
clearLabel
Description
Accessible name for the clear button.
Type
string
Default
"Clear date"
contentClassName
Description
Extra classes for the popover panel (e.g. to restyle the calendar frame).
Type
string
defaultOpen
Description
Whether the calendar popover is initially open (uncontrolled).
Type
boolean
Default
false
defaultValue
Description
The initially selected date (uncontrolled).
Type
Date
disabled
Description
Whether the field is disabled — not focusable, calendar won't open.
Type
boolean
Default
false
format
Description
date-fns token string driving display, parse, and the placeholder.
Type
string
Default
"MM/dd/yyyy"
id
Description
Id of the text input (pair it with a `<label htmlFor>`).
Type
string
isDateUnavailable
Description
Per-date disabling (holidays, blackout dates).
Type
(date: Date) => boolean
maxDate
Description
Latest selectable date (inclusive, day granularity).
Type
Date
minDate
Description
Earliest selectable date (inclusive, day granularity).
Type
Date
name
Description
Name for a hidden input mirroring the value on form submit as a plain `yyyy-MM-dd` calendar date (the `<input type="date">` wire format — zoneless, never UTC-shifted).
Type
string
onOpenChange
Description
Fires when the calendar popover opens or closes.
Type
(open: boolean) => void
onValueChange
Description
Fires with the new date on pick, typed commit, or clear (`undefined` when cleared).
Type
(date: Date | undefined) => void
open
Description
Whether the calendar popover is open. To let the picker manage it, use `defaultOpen` instead.
Type
boolean
placeholder
Description
Field placeholder; defaults to the `format` pattern.
Type
string
presets
Description
Quick-pick shortcuts rendered beside the calendar (e.g. Today, Yesterday).
Type
DatePreset[]
readOnly
Description
Whether the value is read-only: the calendar won't open and typing is ignored, but the field stays focusable so the value is selectable/copyable.
Type
boolean
Default
false
ref
Description
Ref to the text input element.
Type
React.Ref<HTMLInputElement>
required
Description
Whether a date is required — marks the visible field `required` + `aria-required`.
Type
boolean
triggerLabel
Description
Accessible name for the calendar toggle button.
Type
string
Default
"Open calendar"
value
Description
The selected date. To let the picker manage its own value, use `defaultValue` instead.
Type
Date

Plus 3 inherited native <input> attributes.

DateRangePicker

"aria-invalid"
Description
Marks the field invalid — colors the surface and sets `aria-invalid` on the input.
Type
"false" | "true" | boolean
"aria-label"
Description
Accessible name for the field when there is no visible label.
Type
string
"aria-labelledby"
Description
Id(s) of element(s) labelling the field.
Type
string
className
Description
Extra classes for the field surface (the bordered wrapper).
Type
string
clearable
Description
Whether to show a clear ✕ when a range is set.
Type
boolean
Default
true
clearLabel
Description
Accessible name for the clear button.
Type
string
Default
"Clear dates"
contentClassName
Description
Extra classes for the popover panel (e.g. to restyle the calendar frame).
Type
string
defaultOpen
Description
Whether the calendar popover is initially open (uncontrolled).
Type
boolean
Default
false
defaultValue
Description
The initially selected range (uncontrolled).
Type
DateRangeValue
disabled
Description
Whether the field is disabled — not focusable, calendar won't open.
Type
boolean
Default
false
format
Description
date-fns token string applied to both ends + the placeholder.
Type
string
Default
"MM/dd/yyyy"
formatValue
Description
Overrides what the field shows *at rest* — for a summary `format` can't express, like collapsing a year the two ends share ("Aug 1 – Aug 31, 2025"). Editing is unaffected: taking focus re-expands the field to the canonical `format` text, so typed entry still round-trips through `format` alone.
Type
(range: DateRangeValue) => string
id
Description
Id of the text input (pair it with a `<label htmlFor>`).
Type
string
isDateUnavailable
Description
Per-date disabling (holidays, blackout dates).
Type
(date: Date) => boolean
maxDate
Description
Latest selectable date (inclusive, day granularity).
Type
Date
minDate
Description
Earliest selectable date (inclusive, day granularity).
Type
Date
name
Description
Base name for hidden inputs mirroring the range on form submit (`<name>-start` / `<name>-end`), each a plain `yyyy-MM-dd` calendar date.
Type
string
numberOfMonths
Description
Number of month grids shown side by side.
Type
number
Default
2
onOpenChange
Description
Fires when the calendar popover opens or closes.
Type
(open: boolean) => void
onValueChange
Description
Fires with the new range on pick, typed commit, or clear (`undefined` when cleared).
Type
(range: DateRangeValue | undefined) => void
open
Description
Whether the calendar popover is open. To let the picker manage it, use `defaultOpen` instead.
Type
boolean
placeholder
Description
Field placeholder; defaults to `format – format`.
Type
string
presets
Description
Quick-pick ranges rendered beside the calendar (e.g. Last 7 days).
Type
DateRangePreset[]
readOnly
Description
Whether the value is read-only: the calendar won't open and typing is ignored, but the field stays focusable so the value is selectable/copyable.
Type
boolean
Default
false
ref
Description
Ref to the text input element.
Type
React.Ref<HTMLInputElement>
required
Description
Whether a range is required — marks the visible field `required` + `aria-required`.
Type
boolean
triggerLabel
Description
Accessible name for the calendar toggle button.
Type
string
Default
"Open calendar"
value
Description
The selected range. Either end may be unset. To let the picker manage its own value, use `defaultValue` instead.
Type
DateRangeValue

Plus 5 inherited native HTML attributes.

Styling

Target these data-slots and states, and remap these tokens, to restyle without forking the component.

[data-slot="datepicker"][data-slot="datepicker-input"][data-slot="datepicker-affordances"][data-slot="datepicker-clear"][data-slot="datepicker-trigger"][data-slot="datepicker-presets"]

States

SelectorState
:hoverhover
:focusfocus
:focus-visiblefocus-visible
[aria-invalid=true]invalid
[disabled]disabled
::placeholderplaceholder
PropertyTokenTier
height--garn-control-h-mdsemantic · air
paddingInline--garn-control-px-mdsemantic · air
borderColor--garn-foregroundsemantic
iconInset--garn-control-icon-insetsemantic

Private instance vars (never externalize): --in-px

When to use

Collect a single date (or a date range) in a form where typing is often faster than clicking, but a calendar is still wanted for browsing. Use DateRangePicker for a start/end span with optional presets.

Reach for something else when

  • Always-visible month browsing (use Calendar inline)
  • a time-only value (use TimeField/TimePicker)
  • a date + time together (use DateTimePicker)
  • non-date text (use Input).

Overview

DatePicker is the type-and-pick date field: a text input you can type a date into and a calendar you can pick from, on garn's shared field surface (the same appearance × size grid as Input and Select). A single format prop — date-fns tokens like dd.MM.yyyy or MMM d, yyyy — drives the display, the parse, and the placeholder, so the control round-trips in whatever format you set while the committed value stays a native Date (onValueChange always hands back a Date). Typing parses on blur/Enter and reverts to the last valid value on garbage; picking a day fills the field.

DateRangePicker is the sibling export for a { start, end } range — a dual-month calendar with optional preset shortcuts ("Last 7 days", "This month"). Both share one field + popover core with the Calendar and Popover components, so they can't drift from the rest of the form family. Reach for the bare Calendar instead when the month should always be visible inline; reach for Input when the value isn't a date.

Guidelines

Pair every picker with a Label, and let format document the shape. A placeholder shows the expected pattern, not the field's name — associate a real Label (or Form's wiring). Pick a format that matches your users' locale expectations; it drives typing and display, so they stay in sync.

Constrain to valid dates. Use minDate/maxDate and isDateUnavailable to disable out-of-range and blackout days in the calendar and reject them on typed entry — don't accept-then-error. The calendar's navigation is bounded to the same range.

Signal validity through state. Set aria-invalid on error (the field reddens border + ring) and wire the message via aria-describedby (Form does this). Keep the control keyboard-operable: ArrowDown opens, Enter commits a typed value, Escape closes, and the calendar grid is fully navigable.

Wire format: calendar dates are zoneless. With name, the hidden form input posts a plain yyyy-MM-dd string (the <input type="date"> wire format) — DateRangePicker posts <name>-start/<name>-end in the same shape. The value is never UTC-shifted through an ISO instant; if your backend truly wants an instant, convert the Date from onValueChange yourself (lib/date-io holds the family's serializers).

Best practices

Do
  • Pair with a label and choose a format that fits the locale; hold the value in state (or use defaultValue).
  • Constrain with minDate/maxDate/isDateUnavailable rather than validating after entry.
  • Reflect validation with aria-invalid + a described message; use presets for common ranges.
Don't
  • Don't rely on the format-pattern placeholder as the label.
  • Don't hand-set height/padding — the size variant + --garn-control-* tokens own the grid.
  • Don't reach for a segmented MM/DD/YYYY field here — this is the free-text + calendar path (a configurable format is its advantage).

Content guidelines

  • Label the field with what the date is for ("Start date").
  • Use a format whose pattern reads as an example ("MM/dd/yyyy").
  • Write errors as specific, fixable instructions ("Enter a date on or after today").

Troubleshooting

Typing a date does nothing / it snaps back.

Cause. The typed text didn't match format, so it reverted to the last valid value on blur.

Fix. Type in the exact format (shown as the placeholder), or pick from the calendar.

onValueChange gives a Date with the wrong time / timezone.

Cause. The value is a native Date at local midnight for date-only pickers.

Fix. Treat the value as a calendar date; for date + time use DateTimePicker.

DateRangePicker's value is undefined after picking one day.

Cause. A range needs both ends; onValueChange reports { start, end } with end still undefined until the second pick.

Fix. Read value.start / value.end; the popover closes once both are set.

Accessibility

Role
textbox
ARIA APG
date-picker-dialog
Focus
Border darkens on any focus; keyboard focus adds a neutral ring. On open, focus enters the calendar grid; on close it returns to the field.

Accessibility requirements

warn

Give every date picker an accessible label; the format-pattern placeholder is not a label.

when no associated <label htmlFor>, aria-label, or aria-labelledby

Keyboard

ArrowDownOpen the calendar from the field.
EnterCommit the typed date.
EscapeClose the calendar.
ArrowLeftArrowRightArrowUpArrowDownMove by day / week inside the open calendar grid.
PageUpPageDownPrevious / next month in the calendar.
EnterSpaceSelect the focused day (closes the calendar).
A1.3.1Info and RelationshipsA2.1.1KeyboardAA2.4.7Focus VisibleA4.1.2Name, Role, Value
  • Give the field an accessible label — a <label htmlFor>, aria-label, or aria-labelledby; the placeholder (the format pattern) is not a label.
  • The expected format is announced via a visually-hidden aria-describedby hint.
  • Opening the calendar moves focus to the selected/today day; on close, focus returns to the field.
  • The calendar is react-day-picker's labeled grid (roving focus; days expose aria-selected + disabled).
  • Reflect validation with aria-invalid + a described error message.