Creando una UI Personalizada

Esta guía recorre el patrón para construir tu propia UI de selector de fechas sobre los hooks headless.

El Patrón

Cada hook headless sigue el mismo patrón:

  1. Llama al hook con tus opciones (value, onChange, configuration)
  2. Desestructura el valor de retorno para obtener el estado, valores computados y manejadores
  3. Renderiza tu propio marcado usando los valores retornados
  4. Conecta las interacciones llamando a los manejadores proporcionados

El hook gestiona toda la complejidad — generación del calendario, matemática de fechas, estado de abrir/cerrar, navegación por teclado, lógica de rangos — y tú te centras puramente en la presentación.

Ejemplo: Selector de Fechas Personalizado

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>
);
}

Conceptos Clave

getDayProps

La función getDayProps es el núcleo del renderizado del calendario. Para cada celda de fecha, devuelve un objeto DayProps con 18 indicadores booleanos/numéricos que describen el estado de la celda:

const dp = getDayProps(day);
// Estado de selección
dp.isSelected; // Fecha actualmente seleccionada
dp.isToday; // Fecha de hoy
// Estado de rango (para selectores de rango)
dp.isInRange; // Entre las fechas de inicio y fin
dp.isRangeStart; // Primera fecha del rango
dp.isRangeEnd; // Última fecha del rango
dp.isInHoverRange; // En el rango de vista previa del hover
dp.isHoverTarget; // La fecha sobre la que se hace hover
dp.isRangeSingle; // Tanto inicio como fin de rango (rango de un solo día)
// Ayudantes de conexión visual
dp.hasLeftConnection; // Conectado al día anterior (para fondos de rango)
dp.hasRightConnection; // Conectado al día siguiente
dp.isConsecutiveRange; // Parte de un rango de varios días
// Estado
dp.isDisabled; // Deshabilitado por minDate/maxDate/isDateUnavailable
dp.isFocused; // Tiene el foco del teclado
dp.isOutsideDay; // Pertenece al mes adyacente (cuando showOutsideDays está habilitado)
dp.isHighlighted; // En el array highlightDates
// Datos
dp.date; // El objeto Date
dp.day; // Número del día del mes
dp.dayOfWeek; // 0-6 (Domingo-Sábado)

Refs para Clics Externos

Los hooks devuelven containerRef y popupRef. Adjúntalos a tus elementos envolventes y de popup — el hook los usa para detectar clics externos y cerrar el popup automáticamente.

El manejador handleKeyDown soporta las teclas de flecha, Enter, Escape y Tab para una navegación por teclado completa. Adjúntalo al contenedor del popup.

Locale

Todas las cadenas de texto orientadas al usuario provienen del objeto locale. Puedes personalizar cualquier cadena pasando una opción locale parcial:

const picker = useDatePicker({
value,
onChange: setValue,
locale: {
confirm: "OK",
cancel: "Back",
placeholder: "Pick a date...",
},
});

Próximos Pasos