Trang này chưa có bản tiếng Việt. Nội dung dưới đây là bản tiếng Anh.
Composites
Date Picker
A typed date field with a calendar attached, and a range variant with a preset column for reports and list filters. One date-fns locale moves both halves of the control: the calendar language and the day–month order the input expects. It comes from ComitorLocaleProvider — English and MM/dd/yyyy without one — and a locale prop overrides it for a single field.
Single date
The field is a real <input>, not a button that only opens a calendar. Professional data entry — accounting, HR — types faster than it clicks, and a 1975 birthday behind fifty calendar flips is unusable. Type 03/15/2026 and press Enter, or open the calendar; both paths land in the same onValueChange.
value = 2026-05-21
What typing actually commits
Three rules, and they are the reason the field is safe to hand to a fast typist. Try them in the demo above.
- A half-typed string is never overwritten. Keystrokes only move local text state; the component reformats the input from
valueonly when the value, the pattern or the locale changes from outside. - Commit happens on blur and on Enter —
parse()against the same pattern the placeholder advertises, thenstartOfDay()so two dates picked on different clocks compare equal. - An unparseable or out-of-range entry restores the current value rather than silently emitting
null. Nothing is swallowed; the user sees their old date come back. Clearing the field to empty is a deliberatenull. - Changing the language mid-session — the provider's or the
localeprop — re-renders the text through the new pattern. Without that, a string still reading15/03/2026in a field now expectingMM/dd/yyyywould parse as a different day on the next blur.
Locale drives the calendar and the input order
The date-fns locale is the locale prop when you pass one; otherwise it is the one the nearest ComitorLocaleProvider carries — vi for a Vietnamese locale, enUS for every other code and when there is no provider, unless the provider was handed its own dateFnsLocale. (Until 3.0.0 the fallback was a hard-coded vi.) dateFormatForLocale() then reads locale.formatLong.date({ width: 'short' }) and widens a lone y to yyyy — not to change the output (y already prints 2026) but because the same string is the placeholder, and a hint reading dd/MM/y teaches people to type wrong. The derived pattern feeds format, parse and the placeholder at once, so the three cannot drift apart; dateFormat pins it when one screen must ignore the language.
no provider → MM/dd/yyyy
locale="vi" → dd/MM/yyyy
locale="en-GB" + enGB → dd/MM/yyyy
locale="vi" · locale={ja} → yyyy/MM/dd
The prop and the provider are not equals. The provider sets both halves of the language: the date-fns locale and the interface strings — the screen-reader labels, the preset names, the range placeholder. The locale prop is a date-fns Locale, which carries date spelling and nothing else, so it wins for dates only: the last picker above has a Japanese calendar and pattern, and its buttons still announce Mở lịch. Choose the language once, with the provider; keep the prop for the rare field that must differ from it.
| Locale | dateFormatForLocale() | 21 May 2026 renders as |
|---|---|---|
enUSno provider, or any locale but vi | MM/dd/yyyy | 05/21/2026 |
viComitorLocaleProvider locale="vi" | dd/MM/yyyy | 21/05/2026 |
enGBdateFnsLocale={enGB} on the provider | dd/MM/yyyy | 21/05/2026 |
jaany other date-fns locale, via either route | yyyy/MM/dd | 2026/05/21 |
VI_DATE_FORMAT (dd/MM/yyyy) is still exported but is no longer the default. Use it only to pin Vietnamese dates regardless of app language — dateFormat={VI_DATE_FORMAT} on a printed invoice, a tax export. Everywhere else, let the locale decide.
Presets
A preset is { id, label, getValue } where getValue is called at click time, not at module load — so “Today” is still today in a tab left open overnight. The single picker ships with the column hidden (presets={false}); pass an array to turn it on. The demo passes the package's three plus one of its own.
value = null
The two shipped constants carry English labels, but a picker does not print those as they are: it recognises the package's own preset objects by reference and names them from the provider's datePicker.presets table — so a spread copy of the array, or one run through .filter(), still translates. A preset your app builds is shown exactly as written, even when its id is today and even as { ...preset, label } — which is how you rename one, and why your own presets need your own translation. Each constant, with what it reads under locale="vi":
- DEFAULT_DATE_PRESETS
Today · Hôm nayYesterday · Hôm qua7 days ago · 7 ngày trước- DEFAULT_DATE_RANGE_PRESETS — applied automatically by DateRangePicker
Today · Hôm nayLast 7 days · 7 ngày quaLast 30 days · 30 ngày quaThis month · Tháng nàyLast month · Tháng trước
Bounds
minDate and maxDate are enforced twice — the calendar disables the days, and a typed date outside the window is rejected on blur. The demo below is pinned to Q1 2026; try typing 06/01/2026 and tabbing away.
value = 2026-02-10 · window = 2026-01-01 → 2026-03-31
Internally the two bounds become an array of react-day-picker matchers ([{ before }, { after }]), never one object. Putting before and after in the same object makes a DateInterval, which means “the days between” — the exact inverse of the intent. Worth knowing if you ever pass matchers to the raw Calendar primitive yourself.
Date range
The range variant leads with presets, because nine times out of ten the answer is “last 7 days” rather than an arbitrary window — charging two calendar clicks for every report view is a tax on repetition. The trigger is a button showing both formatted dates; there is no typing here.
from = — · to = —
Two traps in one component. First, presets defaults to DEFAULT_DATE_RANGE_PRESETS, and their names — like the placeholder and the clear button's name — follow the provider, not the locale prop. A picker given locale={vi} with no provider above it shows a Vietnamese calendar beside an English preset column; set the language with the provider instead. Second, onValueChange fires on the first calendar click, delivering { from } with no to. Guard on range?.to before you issue a query, or you will fetch on every half-picked range.
Setting its width
className goes to the outer frame — the relative w-full box that also holds the clear button — so className="w-60" sizes the whole picker and the trigger inside stays w-full. DatePicker has always worked that way; DateRangePicker caught up in 3.13.2. Before that, className reached the trigger, so w-60 shrank the button while its frame still took the whole flex row, and the field beside it wrapped onto a line of its own. The flip side: a border or a colour passed through className now lands on the frame, not on the button.
One month, no presets
In a drawer or a narrow filter rail, drop to numberOfMonths={1} and presets={false}. The preset column already collapses to a wrapping chip row below the sm breakpoint, so turn it off only when the shortcuts genuinely do not apply.
The labels contract
DatePickerLabels has three keys and every one of them is screen-reader-only — none of them shows as visible text. That is exactly why the wrong language here goes unnoticed: a picker looks correct in a screenshot while announcing another language, and nothing goes red. They come from the provider's datePicker namespace, so the provider that sets the calendar sets them too; the locale prop cannot — a date-fns locale carries date spelling, not interface strings. The order is the usual one: labels → provider → English.
| Key | Default (en) | Under locale="vi" | Read out on |
|---|---|---|---|
clear | Clear date | Xoá ngày | DatePicker — the × button beside the input |
clearRange | Clear date range | Xoá khoảng ngày | DateRangePicker — the × button on the trigger |
openCalendar | Open calendar | Mở lịch | DatePicker — the calendar-icon button |
labelsisPartial<DatePickerLabels>, merged over the provider's table, so overriding one key keeps the other two — in the provider's language.DEFAULT_DATE_PICKER_LABELSis still exported, but it is the English table for your own use; the component no longer reads it. The Vietnamese one iscomitorMessagesVi.datePicker.- No key here interpolates a number or a unit, so all three are plain strings — unlike
TablePaginationLabels, where such keys are functions. placeholderis not a label. It is its own prop, because it is a per-field string (“Select a report period”) rather than a per-language one. Left unset,DatePickershows the resolved pattern andDateRangePickershows the provider'srangePlaceholder— Pick a date range, or Chọn khoảng ngày in Vietnamese.
import { enGB } from 'date-fns/locale'
import { ComitorLocaleProvider, DatePicker, DateRangePicker } from '@comitor/ui'
// The provider sets both halves at once: the calendar and the pattern (its
// date-fns locale) AND these sr-only strings (its datePicker namespace) —
// here Xóa ngày and Mở lịch.
<ComitorLocaleProvider locale="vi">
<DatePicker value={date} onValueChange={setDate} />
</ComitorLocaleProvider>
// labels is Partial<DatePickerLabels>, merged over the provider's table, so
// one key can be reworded without losing the other two.
<DatePicker value={date} onValueChange={setDate} labels={{ openCalendar: 'Pick the invoice date' }} />
// The locale prop is a date-fns Locale — date spelling, not interface strings.
// British dates here, but the labels are still whatever the provider says.
<DatePicker value={date} onValueChange={setDate} locale={enGB} />
<DateRangePicker
value={range}
onValueChange={setRange}
labels={{ clearRange: 'Clear report period' }}
placeholder="Select a report period" // NOT part of labels — its own prop
/>Reading the constants from a Server Component
The pickers themselves are 'use client', but the constants live in a separate module that deliberately is not. That matters: every export of a 'use client' file reaches a Server Component as a client-reference proxy that reads as undefined without throwing and without warning. Plain data — default labels, presets, format helpers — is exactly what server pages read most, so it is kept outside that boundary.
// date-picker-constants has no 'use client' on purpose, so a Server Component
// can read the presets and label defaults directly.
import { enUS } from 'date-fns/locale'
import { DEFAULT_DATE_RANGE_PRESETS, VI_DATE_FORMAT, dateFormatForLocale, getComitorMessages } from '@comitor/ui'
export default async function Page() {
const pattern = dateFormatForLocale(enUS) // 'MM/dd/yyyy' — safe on the server
const presetIds = DEFAULT_DATE_RANGE_PRESETS.map((preset) => preset.id)
// The constants hold the ENGLISH strings. Any language's table is one pure call away:
const strings = getComitorMessages('vi').datePicker // { clear: 'Xóa ngày', …, presets: { today: 'Hôm nay', … } }
// ...
}
// VI_DATE_FORMAT is still exported, but it is NOT the default any more.
// Reach for it only to pin Vietnamese dates regardless of app language —
// a printed invoice, a tax export file.
VI_DATE_FORMAT // 'dd/MM/yyyy'DatePicker props
| Prop | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
value | Date | null | — | The selected date. Fully controlled — the component stores only the in-progress text of the input, never a date of its own. |
onValueChangebắt buộc | (value: Date | null) => void | — | Called with a startOfDay-normalised date, or null when the field is cleared or emptied. |
placeholder | string | the resolved pattern | Defaults to the format pattern itself — MM/dd/yyyy with no provider, dd/MM/yyyy under a Vietnamese one — which doubles as a typing hint. |
disabled | boolean | false | Disables the input and the calendar trigger, and hides the clear button. |
minDate | Date | — | Earliest selectable day. Disabled in the calendar and rejected on typed entry. |
maxDate | Date | — | Latest selectable day, same two enforcement points as minDate. |
clearable | boolean | true | Shows the × button when a value is present and the field is enabled. |
presets | DatePreset[] | false | false | Shortcut column to the left of the calendar. Off by default for the single picker; pass DEFAULT_DATE_PRESETS for the package's three, named in the provider's language. |
locale | Locale (date-fns) | from provider | Overrides the date-fns locale of ComitorLocaleProvider (enUS, or vi under a Vietnamese provider) for this field: calendar language AND input day–month order via dateFormatForLocale(). Dates only — the sr-only labels and preset names still follow the provider. |
dateFormat | string | from locale | Escape hatch that pins the typed/displayed pattern. Rarely needed — leaving it out is almost always right. |
labels | Partial<DatePickerLabels> | from provider | Screen-reader-only strings for the clear and open-calendar buttons, merged over the provider's datePicker table — English without a provider, the same strings as DEFAULT_DATE_PICKER_LABELS. |
className | string | — | Merged onto the relative wrapper around the input, not onto the input itself. |
id | string | — | Forwarded to the <input>, so an external <Label htmlFor> points at the real control. |
name | string | — | Forwarded to the <input> for uncontrolled form serialisation. |
aria-label | string | — | Forwarded to the <input>. Use it when there is no visible label. |
aria-describedby | string | — | Forwarded to the <input> — point it at your helper or error text. |
aria-invalid | true | — | Literal true only; the type deliberately excludes false and "false". Pass aria-invalid={errors.date ? true : undefined}. |
aria-required | true | — | Literal true only, same shape as aria-invalid. |
DateRangePicker props
| Prop | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
value | DateRange | null | — | The selected range. DateRange is { from: Date | undefined; to?: Date } re-exported from @comitor/ui, so you never import react-day-picker yourself. Note from is a required key whose value may be undefined — {} is not a DateRange. |
onValueChangebắt buộc | (value: DateRange | null) => void | — | Fires on every calendar click, so a half-picked range arrives as { from } with no to. Guard on range?.to before querying. |
placeholder | string | 'Pick a date range' | Shown on the trigger while empty. NOT part of labels: unset, it comes from the provider's rangePlaceholder key (Chọn khoảng ngày in Vietnamese). |
disabled | boolean | false | Disables the trigger and hides the clear button. |
minDate | Date | — | Earliest selectable day. |
maxDate | Date | — | Latest selectable day. |
clearable | boolean | true | Shows the × button once a range is present. |
presets | DateRangePreset[] | false | DEFAULT_DATE_RANGE_PRESETS | Shortcut column, ON by default here. The package's presets are named in the provider's language; presets you build show their labels as written. false drops the column. |
numberOfMonths | number | 2 | Months shown side by side. Two is right for report periods; drop to 1 in a narrow drawer. |
locale | Locale (date-fns) | from provider | Same dual role as on DatePicker — calendar language and the order of the two formatted dates on the trigger — and the same limit: dates only. |
dateFormat | string | from locale | Pins the display pattern regardless of locale. |
labels | Partial<DatePickerLabels> | from provider | Only clearRange is read here — the trigger is its own calendar opener, so openCalendar is unused. |
className | string | — | Merged onto the outer relative w-full frame, as on DatePicker, so className="w-60" sizes the whole picker; the trigger inside is always w-full. Until 3.13.2 it reached the trigger instead — a border or colour meant for the button now lands on the frame. |
id | string | — | Forwarded to the trigger button. |
aria-label | string | — | Forwarded to the trigger button. |
aria-describedby | string | — | Forwarded to the trigger button. |
aria-invalid | true | — | Literal true only, matching DatePicker. There is no aria-required here. |
Types and constants
import type {
DatePickerLabels,
DatePreset,
DateRange, // re-exported from react-day-picker so you never depend on it
DateRangePreset,
} from '@comitor/ui'
interface DatePreset { id: string; label: string; getValue: () => Date }
interface DateRangePreset { id: string; label: string; getValue: () => DateRange }
interface DatePickerLabels { clear: string; clearRange: string; openCalendar: string }
// Values
DEFAULT_DATE_PICKER_LABELS // the three sr-only strings in English — for your code; the pickers read the provider
DEFAULT_DATE_PRESETS // 3 single-date shortcuts; English labels, shown in the provider's language
DEFAULT_DATE_RANGE_PRESETS // 5 range shortcuts, same rule, applied by default
VI_DATE_FORMAT // 'dd/MM/yyyy' — no longer the default pattern
dateFormatForLocale(locale) // derive the numeric pattern from a date-fns Locale
comitorMessagesVi.datePicker // the Vietnamese table: clear, clearRange, openCalendar, rangePlaceholder, presetsAccessibility
- The single picker is a genuine text input — reachable, editable and dictatable without ever opening the calendar. It carries
inputMode="numeric"for the mobile keypad andautoComplete="off"so browser suggestions do not fight the format. - Both trailing icons are real
Buttons carryingsr-onlytext fromlabelsor the provider; the icons themselves arearia-hidden. They are in the tab order, so a keyboard user can clear the field or open the calendar without a mouse. - The calendar's own screen-reader names — the month buttons, each day cell (“Thursday, May 21st, 2026, selected”) — come from the provider's
calendarnamespace. Before 3.0.0 a Vietnamese picker still read react-day-picker's built-in English there, because a date-fns locale carries no interface strings. - The icon row is
pointer-events-nonewithpointer-events-autorestored on each button. Without that, the row's padding would cover the right edge of the input and clicking there would fail to place the caret. - In
DateRangePickerthe clear button is a sibling of the popover trigger, not a child of it. A button inside a button is invalid HTML and unreachable by Tab; as a sibling it gets a real focus ring and a 24px hit area. - The calendar takes
autoFocuswhen the popover opens, so keyboard users land on the day grid; react-day-picker then owns arrow keys, Home/End and PageUp/PageDown. Escape closes the popover and returns focus to the trigger. - The selected day uses the checked-control token pair —
--control-onfor the fill and--control-checkfor the numeral — the same pairing as a checked Checkbox. Measured: the day number reaches 10.19:1 in the default palette and 5.77:1 in high contrast; the cell fill reaches 3.28:1 against the calendar surface in high contrast (1.86:1 in the default palette, an accepted exception in the same class as the Switch track). - Known gap, deliberately open: the today marker and the middle of a selected range use
bg-accent, which is 1.09:1 (light) / 1.17:1 (dark) against the calendar. Pushing the range middle to 3:1 turns a whole week into a solid block, so this is a design question rather than a token swap. Do not rely on the today tint alone to convey anything. aria-invalidandaria-requiredaccept the literaltrueonly — writearia-invalid={errors.date ? true : undefined}rather than passing a boolean expression.- Give every picker a name: an
idpaired with a visibleLabel htmlFor, oraria-labelwhen there is no visible label. Pointaria-describedbyat your error text so a rejected entry is explained rather than merely reverted. - Text is 16px below
mdand 14px frommdup —DatePickerbecause it is anInput, the range trigger because it setstext-base md:text-smitself, the rule every field shares since 3.13.4. Leave the size to the package; why is on Input.