Création d’une interface utilisateur personnalisée

Ce guide présente le modèle pour construire votre propre interface utilisateur de sélecteur de dates par-dessus les hooks headless.

Le Modèle

Chaque hook headless suit le même modèle :

  1. Appelez le hook avec vos options (value, onChange, configuration)
  2. Déstructurez la valeur de retour pour obtenir l’état, les valeurs calculées et les gestionnaires
  3. Rendez votre propre balisage en utilisant les valeurs retournées
  4. Connectez les interactions en appelant les gestionnaires fournis

Le hook gère toute la complexité — génération du calendrier, calcul des dates, état ouvert/fermé, navigation au clavier, logique de plage — et vous vous concentrez uniquement sur la présentation.

Exemple : Sélecteur de dates personnalisé

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

Concepts Clés

getDayProps

La fonction getDayProps est au cœur du rendu du calendrier. Pour chaque cellule de date, elle retourne un objet DayProps avec 18 indicateurs booléens/numériques qui décrivent l’état de la cellule :

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 pour le clic extérieur

Les hooks retournent containerRef et popupRef. Attachez-les à vos éléments d’enveloppe et de popup — le hook les utilise pour détecter les clics extérieurs et fermer automatiquement la popup.

Le gestionnaire handleKeyDown prend en charge les touches fléchées, Entrée, Échap et Tab pour une navigation complète au clavier. Attachez-le au conteneur de la popup.

Locale

Toutes les chaînes de caractères visibles par l’utilisateur proviennent de l’objet locale. Vous pouvez personnaliser n’importe quelle chaîne en passant une option locale partielle :

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

Prochaines Étapes

  • Hook Reference — Options complètes et valeurs de retour pour chaque hook
  • Contexts — Utiliser les fournisseurs pour les modèles de composants composés
  • Date Utilities — Fonctions utilitaires pour la manipulation des dates