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:

  1. Call the hook with your options (value, onChange, configuration)
  2. Destructure the return value to get state, computed values, and handlers
  3. Render your own markup using the returned values
  4. 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 state
dp.isSelected; // Currently selected date
dp.isToday; // Today's date
// Range state (for range pickers)
dp.isInRange; // Between start and end dates
dp.isRangeStart; // First date of the range
dp.isRangeEnd; // Last date of the range
dp.isInHoverRange; // In the hover preview range
dp.isHoverTarget; // The hovered date itself
dp.isRangeSingle; // Both range start and range end (single-day range)
// Visual connection helpers
dp.hasLeftConnection; // Connected to previous day (for range backgrounds)
dp.hasRightConnection; // Connected to next day
dp.isConsecutiveRange; // Part of a multi-day range
// State
dp.isDisabled; // Disabled by minDate/maxDate/isDateUnavailable
dp.isFocused; // Has keyboard focus
dp.isOutsideDay; // Belongs to adjacent month (when showOutsideDays is enabled)
dp.isHighlighted; // In the highlightDates array
// Data
dp.date; // The Date object
dp.day; // Day of month number
dp.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