Building Custom UI
This guide walks through the pattern for building your own date picker UI on top of the headless hooks.
The Pattern
Every headless hook follows the same pattern:
- Call the hook with your options (value, onChange, configuration)
- Destructure the return value to get state, computed values, and handlers
- Render your own markup using the returned values
- Wire up interactions by calling the provided handlers
The hook manages all the complexity — calendar generation, date math, open/close state, keyboard navigation, range logic — and you focus purely on presentation.
Example: Custom Date Picker
import { useState } from "react";import { useDatePicker } from "react-date-range-picker-headless";
function CustomDatePicker() { const [value, setValue] = useState<Date | null>(null);
const { isOpen, calendar, getDayProps, displayValue, hasValue, canConfirm, locale, handleToggle, handleDateClick, handleConfirm, handleCancel, handleClear, handlePrevMonth, handleNextMonth, handleKeyDown, containerRef, popupRef, } = useDatePicker({ value, onChange: setValue });
return ( <div ref={containerRef} style={{ position: "relative", display: "inline-block" }}> {/* Trigger */} <button onClick={handleToggle} style={{ padding: "8px 16px", border: "1px solid #d1d5db", borderRadius: 6, background: "white", cursor: "pointer", minWidth: 180, textAlign: "left", }} > {displayValue || locale.placeholder} </button>
{/* Popup */} {isOpen && ( <div ref={popupRef} onKeyDown={handleKeyDown} style={{ position: "absolute", top: "100%", left: 0, marginTop: 4, padding: 16, background: "white", border: "1px solid #e5e7eb", borderRadius: 8, boxShadow: "0 4px 12px rgba(0,0,0,0.1)", zIndex: 50, }} > {/* Header */} <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12, }} > <button onClick={handlePrevMonth}>{locale.prevMonth}</button> <span style={{ fontWeight: 600 }}>{locale.formatMonthYear(calendar.month)}</span> <button onClick={handleNextMonth}>{locale.nextMonth}</button> </div>
{/* Weekday headers */} <div style={{ display: "grid", gridTemplateColumns: "repeat(7, 36px)", gap: 2, textAlign: "center", }} > {locale.weekdays.map((wd) => ( <span key={wd} style={{ fontSize: 12, color: "#9ca3af", padding: 4 }}> {wd} </span> ))} </div>
{/* Calendar grid */} <div style={{ display: "grid", gridTemplateColumns: "repeat(7, 36px)", gap: 2 }}> {calendar.weeks.flat().map((day, i) => { if (!day) return <span key={i} />; const dp = getDayProps(day); return ( <button key={i} onClick={() => handleDateClick(day)} disabled={dp.isDisabled} style={{ width: 36, height: 36, borderRadius: "50%", border: dp.isFocused ? "2px solid #0ea5e9" : "none", background: dp.isSelected ? "#0ea5e9" : dp.isToday ? "#f0f9ff" : "transparent", color: dp.isSelected ? "white" : dp.isDisabled ? "#d1d5db" : "inherit", cursor: dp.isDisabled ? "not-allowed" : "pointer", fontWeight: dp.isToday ? 600 : 400, }} > {dp.day} </button> ); })} </div>
{/* Footer */} <div style={{ display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 12 }}> {hasValue && ( <button onClick={handleClear} style={{ color: "#ef4444" }}> {locale.clear} </button> )} <button onClick={handleCancel}>{locale.cancel}</button> <button onClick={handleConfirm} disabled={!canConfirm} style={{ background: canConfirm ? "#0ea5e9" : "#e5e7eb", color: canConfirm ? "white" : "#9ca3af", padding: "4px 16px", borderRadius: 4, border: "none", }} > {locale.confirm} </button> </div> </div> )} </div> );}Key Concepts
getDayProps
The getDayProps function is the core of calendar rendering. For each date cell, it returns a DayProps object with 18 boolean/number flags that describe the cell’s state:
const dp = getDayProps(day);
// Selection statedp.isSelected; // Currently selected datedp.isToday; // Today's date
// Range state (for range pickers)dp.isInRange; // Between start and end datesdp.isRangeStart; // First date of the rangedp.isRangeEnd; // Last date of the rangedp.isInHoverRange; // In the hover preview rangedp.isHoverTarget; // The hovered date itselfdp.isRangeSingle; // Both range start and range end (single-day range)
// Visual connection helpersdp.hasLeftConnection; // Connected to previous day (for range backgrounds)dp.hasRightConnection; // Connected to next daydp.isConsecutiveRange; // Part of a multi-day range
// Statedp.isDisabled; // Disabled by minDate/maxDate/isDateUnavailabledp.isFocused; // Has keyboard focusdp.isOutsideDay; // Belongs to adjacent month (when showOutsideDays is enabled)dp.isHighlighted; // In the highlightDates array
// Datadp.date; // The Date objectdp.day; // Day of month numberdp.dayOfWeek; // 0-6 (Sunday-Saturday)Refs for Click-Outside
The hooks return containerRef and popupRef. Attach these to your wrapper and popup elements — the hook uses them to detect outside clicks and close the popup automatically.
Keyboard Navigation
The handleKeyDown handler supports arrow keys, Enter, Escape, and Tab for full keyboard navigation. Attach it to the popup container.
Locale
All user-facing strings come from the locale object. You can customize any string by passing a partial locale option:
const picker = useDatePicker({ value, onChange: setValue, locale: { confirm: "OK", cancel: "Back", placeholder: "Pick a date...", },});Next Steps
- Hook Reference — Full options and return values for each hook
- Contexts — Use providers for compound component patterns
- Date Utilities — Helper functions for date manipulation