Retour au catalogue

Contact Calendar Embed

Section contact avec selecteur de creneaux horaires style calendrier. L'utilisateur choisit un jour et un horaire pour un rendez-vous.

contactcomplex Both Responsive a11y
corporateelegantminimalsaasagencymedicallegaluniversalsplit
Theme

Créer un sélecteur de date et de créneaux horaires en React

Un calendrier de réservation React construit une grille mensuelle via l'arithmétique native de Date, suit le jour et le créneau sélectionnés dans un état local, et anime les transitions de créneaux avec AnimatePresence de Framer Motion. Aucune librairie calendrier externe n'est nécessaire.

  • Stack : React 18, Framer Motion 11, Lucide React, Tailwind v4, ~230 lignes, sans librairie calendrier tierce.
  • Grille mensuelle calculée via getDaysInMonth et getFirstDayOfWeek en Date native, avec décalage semaine lundi-premier.
  • Les jours de week-end sont désactivés automatiquement via getDay() ; l'état désactivé est stylé en opacity-30.
  • Accessible : les boutons portent l'attribut natif disabled ; la navigation clavier suit l'ordre de tabulation entre calendrier et créneaux.
  • Le panneau de créneaux entre/sort avec AnimatePresence mode='wait'. Changer key={selectedDay} déclenche une animation de réentrée à chaque nouveau jour sélectionné.

Contact Calendar Embed est une section React autonome qui permet aux visiteurs de choisir un jour et un créneau de réunion sans quitter la page ni charger un widget tiers. Une grille mensuelle à gauche et un panneau de créneaux animé à droite rendent le flux en deux étapes immédiatement lisible. Il convient aux flows d'onboarding SaaS, aux pages contact d'agences, et à tout service professionnel qui a besoin d'une surface de réservation intégrée.

Anatomie

La section contient un en-tête centré (label eyebrow, h2, description optionnelle) et une grille CSS à 5 colonnes en dessous. Le panneau calendrier occupe 3 colonnes : une rangée de navigation mensuelle en haut, une rangée d'en-têtes des 7 jours, puis les boutons de jours dans une sous-grille à 7 colonnes avec des cellules vides initiales pour aligner correctement le premier jour de la semaine. Le panneau créneaux (2 colonnes) affiche une barre de contexte avec la date sélectionnée et la durée, puis soit un état vide, soit une grille 2 colonnes de boutons d'horaires et un CTA de confirmation une fois un créneau choisi.

Comment ça marche

Toute la logique calendrier repose sur useState et deux fonctions pures. getDaysInMonth(year, month) appelle new Date(year, month+1, 0).getDate() pour obtenir le dernier jour. getFirstDayOfWeek retourne un décalage lundi-zéro (0–6) en remappant dimanche de 0 à 6. Sélectionner un jour écrit dans l'état selectedDay et réinitialise selectedSlot ; le bloc AnimatePresence du panneau créneaux reçoit key={selectedDay}, donc React démonte l'ancienne liste et monte la nouvelle, déclenchant la transition de fondu. Quand un créneau est confirmé, un bouton Framer Motion glisse depuis y:8 avec la constante de spring E=[0.16,1,0.3,1]. Toutes les couleurs viennent de propriétés CSS custom (--color-accent, --color-background-alt, etc.) pour que le composant s'adapte à n'importe quel preset de thème sans modifier le code.

Comment le coder en React

  1. Calculer la grille mensuelle

    Deux fonctions pures gèrent le calcul calendrier. getDaysInMonth utilise l'astuce d'overflow Date : passer le jour 0 du mois+1 retourne le dernier jour du mois courant. getFirstDayOfWeek remet dimanche (jour JS 0) en position 6 pour que les semaines commencent le lundi. Construis deux tableaux, les jours réels et un préfixe de cellules vides, pour alimenter la grille à 7 colonnes.

    function getDaysInMonth(year: number, month: number) {
      return new Date(year, month + 1, 0).getDate();
    }
    function getFirstDayOfWeek(year: number, month: number) {
      const day = new Date(year, month, 1).getDay();
      return day === 0 ? 6 : day - 1;
    }
  2. Connecter la navigation mensuelle et la sélection de jour

    Stocke month, year, selectedDay et selectedSlot dans quatre appels useState. Les handlers prev/next gèrent le passage décembre/janvier et réinitialisent la sélection. Les boutons de jours appellent isWeekend(day) pour désactiver samedi et dimanche, en gardant la prop disabled sur le bouton natif afin que les utilisateurs clavier ne puissent pas atteindre ces cellules.

    const [month, setMonth] = useState(today.getMonth());
    const [selectedDay, setSelectedDay] = useState<number | null>(null);
    const isWeekend = (day: number) => {
      const d = new Date(year, month, day).getDay();
      return d === 0 || d === 6;
    };
  3. Animer le panneau de créneaux avec AnimatePresence

    Enveloppe la liste de créneaux dans un bloc AnimatePresence mode='wait'. Donne à la motion.div une key={selectedDay}, quand le jour change React détruit les anciens enfants et monte les nouveaux, exécutant d'abord l'animation exit puis l'entrée. Une simple transition opacity:0 à opacity:1 évite les décalages de mise en page tout en donnant un retour visuel.

    <AnimatePresence mode="wait">
      {selectedDay ? (
        <motion.div
          key={selectedDay}
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          className="grid grid-cols-2 gap-2"
        >
          {timeSlots.map((slot) => (
            <button key={slot} onClick={() => setSelectedSlot(slot)}>
              {slot}
            </button>
          ))}
        </motion.div>
      ) : <EmptyState />}
    </AnimatePresence>
  4. Styler avec des propriétés CSS custom pour la portabilité de thème

    Ne jamais coder les couleurs en dur. Utilise --color-accent pour les fonds jour/créneau sélectionnés, --color-background-alt pour les surfaces des cartes, et --color-border pour les contours. Les sept presets de thème du registry (lime-light, violet-dark, etc.) définissent tous ces tokens, donc le calendrier s'affiche correctement sans aucun override de style.

    style={{
      background: selected ? "var(--color-accent)" : "transparent",
      color: selected ? "var(--color-background)" : "var(--color-foreground)",
    }}

Quand l'utiliser

Utilise cette section quand tu veux que les visiteurs se programment eux-mêmes sans naviguer vers une autre page ni charger Calendly dans une iframe. Elle convient aux pages contact d'agences, aux démos produit SaaS, aux consultations juridiques et médicales, et à tout service facturé à l'heure. À éviter sur les pages e-commerce transactionnelles où un calendrier crée de la friction, et à ignorer si tes données de disponibilité sont dynamiques, ce composant accepte un tableau timeSlots statique et ne se connecte pas à un backend par défaut. Tu devras câbler le bouton de confirmation à une API toi-même.

Utilisé par

  • Calendly, Le produit de référence pour la prise de rendez-vous intégrée : grille mensuelle, sélection de jour, liste de créneaux et étape de confirmation dans un seul panneau.
  • Cal.com, Plateforme de réservation open-source dont le widget embarquable utilise la même mise en page calendrier/créneaux en deux colonnes, entièrement personnalisable via des variables CSS.
  • Stripe, La prise de rendez-vous commerciaux sur stripe.com utilise un sélecteur de date et d'heure minimal avant de rediriger vers un lien d'appel vidéo.
  • Notion, L'éditeur de propriété date de Notion affiche une grille mensuelle avec des boutons de jours et une saisie d'heure qui suit le même modèle d'interaction : choisir le jour, puis l'heure.

FAQ

Ce composant se connecte-t-il à Google Calendar ou Calendly ?

Non. C'est un composant purement UI. La prop timeSlots accepte un tableau de chaînes statique et le bouton de confirmation ne déclenche aucune requête par défaut. Câble un handler onClick à ta propre API ou à un endpoint REST Calendly/Cal.com pour le rendre fonctionnel.

Comment désactiver des jours spécifiques en dehors du week-end ?

Passe une prop disabledDates sous forme de tableau de Date ou de chaînes ISO, puis étends la vérification disabled dans le bouton de jour : `disabled={weekend || isDisabled(day)}`. Le style opacity-30 s'applique déjà sur disabled, donc aucun CSS supplémentaire n'est nécessaire.

Pourquoi changer de mois réinitialise-t-il le créneau sélectionné ?

Les handlers prevMonth et nextMonth appellent explicitement setSelectedDay(null) et setSelectedSlot(null). Le 15 mars et le 15 avril sont deux rendez-vous différents, donc la réinitialisation à la navigation évite des erreurs silencieuses où l'utilisateur se retrouve sur un écran de confirmation pour le mauvais mois.

Peut-on l'utiliser sur mobile ?

La mise en page passe en une colonne sous le breakpoint lg, donc le calendrier et les créneaux s'affichent l'un au-dessus de l'autre. Les événements tactiles fonctionnent bien car l'interaction utilise des handlers click. Les boutons de jours utilisent aspect-square pour des zones de tap adaptées au tactile. Teste sur petits écrans pour t'assurer que ton tableau timeSlots ne déborde pas de la grille à 2 colonnes.

"use client";

import { useState, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronLeft, ChevronRight, Clock, Calendar, Check } from "lucide-react";

interface ContactCalendarEmbedProps {
  title?: string;
  subtitle?: string;
  description?: string;
  timeSlots?: string[];
  daysOfWeek?: string[];
  confirmLabel?: string;
  duration?: string;
}

const E: [number, number, number, number] = [0.16, 1, 0.3, 1];

function getDaysInMonth(year: number, month: number) {
  return new Date(year, month + 1, 0).getDate();
}

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Avis

Calendrier de réservation React avec créneaux horaires, Code