Customizing pickers
Props cover the common changes: renderCell, renderTrigger, presets, footer, tokens and data-* attributes. When they are not enough, compose your own picker from the public parts. The parts and their data-* attributes are stable; the finished pickers’ internal markup may change in minor releases, which is why composing is safer than styling their internals.
A range picker from parts
Section titled “A range picker from parts”This is the complete source of the demo above. It uses only @lunisolar-picker/react:
import type { DateRangeValue } from '@lunisolar-picker/calendar';import type { RangeLimitMode } from '@lunisolar-picker/core';import { Calendar, DateRangeField, Popover, RangeCalendar, useRangeCalendarContext, type ValueChangeDetails,} from '@lunisolar-picker/react';import { useState } from 'react';
export interface MyDateRangePickerProps { readonly endName?: string; readonly label: string; readonly limitMode?: RangeLimitMode; readonly max?: string; readonly maxDays?: number; readonly onValueChange: (value: DateRangeValue | null, details: ValueChangeDetails) => void; readonly startName?: string; readonly value: DateRangeValue | null;}
function ConfirmButtons() { const calendar = useRangeCalendarContext(); return ( <div data-lsp-part="picker-footer"> <Popover.Close>Cancel</Popover.Close> <Popover.Close disabled={!calendar.validity.isValid} onClick={() => calendar.commit()}> Apply </Popover.Close> </div> );}
export function MyDateRangePicker({ endName, label, limitMode, max, maxDays, onValueChange, startName, value,}: MyDateRangePickerProps) { const [isOpen, setIsOpen] = useState(false); const constraints = { limitMode, max, maxDays }; return ( <div data-lsp-part="date-range-picker"> <DateRangeField.Root aria-label={label} value={value} onValueChange={onValueChange} startName={startName} endName={endName} {...constraints} /> <Popover.Root open={isOpen} onOpenChange={setIsOpen}> <Popover.Trigger aria-label={`${label}: choose in calendar`}> <CalendarIcon /> </Popover.Trigger> <Popover.Content aria-label={label}> <RangeCalendar.Root value={value} onValueChange={onValueChange} commitMode="confirm" {...constraints}> <Calendar.Layout /> <ConfirmButtons /> </RangeCalendar.Root> </Popover.Content> </Popover.Root> </div> );}
function CalendarIcon() { return ( <svg aria-hidden="true" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" > <rect x="2" y="3" width="12" height="11" rx="2" /> <path d="M2 6.5h12M5.5 1.5v3M10.5 1.5v3" /> </svg> );}How the pieces fit:
- The field and the calendar are siblings that share one controlled value. Do not wrap the field in
RangeCalendar.Root. RangeCalendar.Rootlives insidePopover.Content, which unmounts on close, so an unconfirmed draft disappears when the popover closes.- Form names (
startName,endName) go on the field only, which is always mounted. Putting them on both parts would submit every value twice. - Children of
RangeCalendar.Rootreplace its default layout, so the recipe renders<Calendar.Layout />itself. Popover.Contenthas no default name. Give it anaria-label.
The finished DateRangePicker adds a clear button, a bottom sheet on phones, form reset and the presets slot on top of this.
Common edits
Section titled “Common edits”Close on pick instead of confirming. Drop commitMode="confirm" and the buttons, control the popover, and close it in onPick, which runs after onValueChange whenever the user completes a range:
<RangeCalendar.Root value={value} onValueChange={onValueChange} onPick={() => setIsOpen(false)}> <Calendar.Layout /></RangeCalendar.Root>Presets. Inside RangeCalendar.Root, useRangeCalendarContext().setRange(range) shows a range, and validity tells you whether the draft is acceptable. (usePickerSelection belongs to the finished DateRangePicker.)
function LastWeek({ range }: { range: DateRangeValue }) { const calendar = useRangeCalendarContext(); return <button type="button" onClick={() => calendar.setRange(range)}>Last 7 days</button>;}Holiday names in cells. Pass renderCell to Calendar.Layout, and getCellLabel to the root so screen readers hear the holiday:
<RangeCalendar.Root value={value} onValueChange={onValueChange} getCellLabel={(cell) => holidays[cell.date]}> <Calendar.Layout renderCell={(info) => ( <> {info.day} {holidays[info.date] && <small>{holidays[info.date]}</small>} </> )} /></RangeCalendar.Root>A filter chip trigger. Pass your chip to Popover.Trigger’s render prop. The chip receives the trigger’s ARIA attributes, event handlers and ref. On a finished picker, use renderTrigger and spread triggerProps on your button.
Two months. Set numberOfMonths={2} on the root. Calendar.Layout renders both; in your own layout put <Calendar.Heading monthIndex={1} /> and <Calendar.Grid monthIndex={1}> in a second data-lsp-part="calendar-month" element while the day grid is showing, as described in Calendar parts.
A warning for long ranges. With limitMode="flag", read validity.reasons from onValueChange, or from useRangeCalendarContext().validity inside the popover, and render your own message.