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:
- Llama al hook con tus opciones (value, onChange, configuration)
- Desestructura el valor de retorno para obtener el estado, valores computados y manejadores
- Renderiza tu propio marcado usando los valores retornados
- 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óndp.isSelected; // Fecha actualmente seleccionadadp.isToday; // Fecha de hoy
// Estado de rango (para selectores de rango)dp.isInRange; // Entre las fechas de inicio y findp.isRangeStart; // Primera fecha del rangodp.isRangeEnd; // Última fecha del rangodp.isInHoverRange; // En el rango de vista previa del hoverdp.isHoverTarget; // La fecha sobre la que se hace hoverdp.isRangeSingle; // Tanto inicio como fin de rango (rango de un solo día)
// Ayudantes de conexión visualdp.hasLeftConnection; // Conectado al día anterior (para fondos de rango)dp.hasRightConnection; // Conectado al día siguientedp.isConsecutiveRange; // Parte de un rango de varios días
// Estadodp.isDisabled; // Deshabilitado por minDate/maxDate/isDateUnavailabledp.isFocused; // Tiene el foco del tecladodp.isOutsideDay; // Pertenece al mes adyacente (cuando showOutsideDays está habilitado)dp.isHighlighted; // En el array highlightDates
// Datosdp.date; // El objeto Datedp.day; // Número del día del mesdp.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.
Navegación por Teclado
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
- Referencia de Hooks — Opciones completas y valores de retorno para cada hook
- Contextos — Usa proveedores para patrones de componentes compuestos
- Utilidades de Fecha — Funciones de ayuda para la manipulación de fechas