Créer une section réservation avec calendrier en React
Une section de réservation React associe un calendrier placeholder à gauche à une liste de boutons de créneaux à droite. La sélection d'un créneau active le bouton de confirmation ; un clic déclenche une animation scale-in Framer Motion vers l'état de succès.
- Stack : React 18 + Framer Motion 11 + Tailwind v4 + lucide-react (icônes Calendar, Clock, Check), ~167 lignes.
- État : deux hooks useState, selectedSlot (string | null) et booked (boolean). Aucune librairie de formulaire requise.
- Animations : le header apparaît avec y:20 au scroll (whileInView, once:true) ; les panneaux calendrier et créneaux glissent depuis les côtés opposés avec des délais décalés.
- Accessible : le bouton de confirmation utilise l'attribut disabled quand aucun créneau n'est sélectionné, empêchant toute soumission sans choix valide.
- Responsive : colonne unique sur mobile, deux colonnes côte à côte à partir du breakpoint lg.
Contact Booking est une section React en split-layout conçue pour convertir les visiteurs en appels planifiés. Un calendrier placeholder occupe la gauche ; une liste de créneaux sélectionnables, la droite. Une fois un créneau choisi et confirmé, le panneau entier se remplace par un écran de succès animé. Le composant couvre tout le flux, parcourir, choisir, confirmer, sans dépendre d'un SDK calendrier externe.
Anatomie
La section est un conteneur centré max-w-5xl structuré en trois couches. En haut, un motion.div header contient un badge, un h2 et un sous-titre, tous centrés. En dessous, une grille deux colonnes (lg:grid-cols-2) sépare le panneau calendrier du panneau créneaux. Le panneau calendrier est une carte arrondie avec un header 7 colonnes pour les jours et une grille de 28 cellules de dates ; la 15e est mise en valeur en couleur accent comme placeholder statique. Le panneau créneaux liste les boutons de créneaux en pleine largeur, suivi du CTA de confirmation. Quand booked est true, la grille laisse place à une carte de succès centrée avec une icône Check, un titre et un texte d'aide.
Comment ça marche
Chaque motion.div utilise le pattern `whileInView` + `viewport={{ once: true }}` pour que les animations se déclenchent une seule fois lorsque la section entre dans le viewport. Le header entre avec `{ opacity: 0, y: 20 }`, se stabilisant en 0.6s. Les panneaux calendrier et créneaux se font miroir : le calendrier glisse depuis x:-20 avec un délai de 0.05s, le panneau créneaux depuis x:20 avec 0.15s, créant une légère convergence. Toutes les transitions partagent le même cubic-bezier `[0.16, 1, 0.3, 1]`. La sélection de créneau est de l'état React pur : cliquer un bouton assigne son id à `selectedSlot` ; le style du bouton passe au fond accent et au texte contrasté. Cliquer confirmer assigne true à `booked`, ce qui démonte la grille et monte la carte de succès via `{ opacity: 0, scale: 0.95 }` animé vers `{ opacity: 1, scale: 1 }`.
Comment le coder en React
Initialiser l'état et la constante ease
Déclare deux valeurs d'état : `selectedSlot` pour suivre le créneau actif, et `booked` pour contrôler l'écran de confirmation. Définis le tuple ease une fois au niveau du module pour que toutes les transitions partagent la même courbe sans la répéter.
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1]; const [selectedSlot, setSelectedSlot] = useState<string | null>(null); const [booked, setBooked] = useState(false);Construire le header animé
Enveloppe le badge, le h2 et le sous-titre dans un seul motion.div avec `initial={{ opacity: 0, y: 20 }}` et `whileInView={{ opacity: 1, y: 0 }}`. Passe `viewport={{ once: true }}` pour que l'animation ne se joue qu'une fois lors du scroll.
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, ease }} viewport={{ once: true }} className="text-center mb-14" >Faire glisser les deux panneaux depuis les côtés opposés
Donne au panneau calendrier `initial={{ x: -20 }}` et au panneau créneaux `initial={{ x: 20 }}`, les deux animés vers `x: 0`. Décale légèrement les délais (0.05s vs 0.15s) pour qu'ils convergent plutôt que d'apparaître simultanément.
// Calendar panel <motion.div initial={{ opacity: 0, x: -20 }} whileInView={{ opacity: 1, x: 0 }} transition={{ duration: 0.5, ease, delay: 0.05 }} viewport={{ once: true }} > // Slots panel <motion.div initial={{ opacity: 0, x: 20 }} whileInView={{ opacity: 1, x: 0 }} transition={{ duration: 0.5, ease, delay: 0.15 }} viewport={{ once: true }} >Gérer la confirmation et l'état de succès
Le bouton de confirmation appelle `setBooked(true)` uniquement si `selectedSlot` n'est pas null ; l'attribut `disabled` bloque sinon. Quand `booked` est true, affiche la carte de succès à la place de la grille, en la montant avec une animation scale-in pour une transition fluide.
{booked ? ( <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.4, ease }} className="rounded-xl p-12 text-center mx-auto max-w-md" > <Check size={40} style={{ color: "var(--color-accent)" }} /> </motion.div> ) : ( <div className="grid lg:grid-cols-2 gap-8">...</div> )}
Quand l'utiliser
Utilise cette section en bas de page sur des sites d'agence, SaaS, consulting ou services médicaux dont l'objectif est de décrocher un appel découverte. Elle fonctionne bien après une section pricing ou témoignages. Évite-la si tu as besoin d'un vrai calendrier (Google Calendar, Calendly) : ce composant a intentionnellement un calendrier placeholder sans logique de sélection de date. Pour les flows de réservation entièrement automatisés, intègre un vrai SDK de planification et ne conserve que le shell de layout.
Utilisé par
- Calendly, L'interface de réservation split-layout de référence : calendrier à gauche, créneaux à droite, étape de confirmation après sélection.
- Cal.com, Outil de planification open-source avec le même layout deux panneaux ; la liste de créneaux et le bouton de confirmation reprennent le même pattern.
- Doctolib, Réservation de rendez-vous médicaux reposant sur un calendrier en grille associé à des boutons de créneaux disponibles et un écran de confirmation clair.
- HubSpot Meetings, Pages de réservation intégrables utilisées par les équipes SaaS B2B ; même pattern de sélection de créneau et de validation pour les appels commerciaux.
FAQ
Comment connecter ce composant à un vrai backend de calendrier ?
Remplace la prop `slots` par des données récupérées depuis ton API (Calendly, Cal.com ou ton propre endpoint de disponibilité). À la confirmation, envoie une requête POST avec l'id du créneau sélectionné avant d'appeler `setBooked(true)`.
Puis-je utiliser un vrai sélecteur de date à la place du calendrier placeholder ?
Oui. Remplace le contenu du panneau calendrier par react-day-picker ou un <input type='date'> natif, puis dérive les créneaux disponibles à partir de la date choisie. Le shell de layout et les animations restent intacts.
Pourquoi le bouton de confirmation reste-t-il désactivé tant qu'aucun créneau n'est sélectionné ?
Le bouton lit `disabled={!selectedSlot}` et le handler vérifie `selectedSlot &&` avant de mettre à jour l'état. Cela évite que l'écran de confirmation apparaisse sans données de réservation réelles, empêchant un état de succès vide et trompeur.
Comment ajouter une réinitialisation pour que les utilisateurs puissent réserver un autre créneau ?
Sur la carte de succès, ajoute un bouton qui appelle `setBooked(false)` et `setSelectedSlot(null)`. La grille sera remontée et Framer Motion relancera les animations d'entrée si tu passes `once: false` sur la prop viewport.