Créer une page de maintenance plein écran avec compte à rebours en React
Une page de maintenance React avec compte à rebours stocke les secondes restantes dans useState, les décrémente avec setInterval dans useEffect, et formate heures/minutes/secondes avec padStart. Framer Motion gère l'entrée en fondu et l'animation de tremblement récurrente de la clé.
- Stack : React 18 + Framer Motion 11 + Lucide React, ~200 lignes, zéro dépendance supplémentaire.
- Timer : setInterval natif via useEffect, nettoyé au démontage, aucune lib de dates tierce.
- Arrière-plan : deux icônes Settings (Lucide) tournent en continu à 3% d'opacité via la prop animate de Framer Motion.
- Accessible : aria-hidden sur les engrenages décoratifs ; les cellules du countdown utilisent tabular-nums pour une largeur de chiffre stable.
- Entièrement responsive, le layout est un flex centré, le countdown utilise inline-flex avec gap ; fonctionne à toute largeur de viewport.
Ce composant est une page de maintenance plein écran conçue pour remplacer toute l'application pendant les travaux. Il affiche un compte à rebours live (durée configurable en minutes), une animation de clé récurrente, des engrenages décoratifs tournants et un lien de contact mailto. La carte entière s'anime à l'entrée avec un ease spring, pour que la page soit soignée même quand le produit est indisponible.
Anatomie
Le layout est une colonne centrée dans une section plein viewport. En haut, un carré d'icône contient la Wrench avec une animation de tremblement répétée. En dessous, titre et sous-titre. Le bloc countdown affiche trois cellules style card (heures, minutes, secondes) en ligne inline-flex. Une ligne de retour estimé avec l'icône Clock suit, puis une liste optionnelle de fonctionnalités en cours de mise à jour sous forme de pill tags, et enfin un ancre mailto avec l'icône Mail.
Comment ça marche
Le compte à rebours dérive d'un seul état : `remaining` (secondes totales). Un useEffect démarre un interval qui le décrémente de 1 toutes les 1000ms et se nettoie au démontage ou quand remaining atteint 0. Les heures, minutes et secondes sont calculées à chaque rendu par division entière et modulo, puis paddées à deux chiffres avec String.padStart. L'animation d'entrée utilise le hook `useInView` de Framer Motion avec `once: true` pour que le contenu monte depuis y:30 seulement lors du premier passage dans le viewport. Les engrenages de fond utilisent `animate={{ rotate: 360 }}` avec `repeat: Infinity` et `ease: "linear"` pour une rotation continue et fluide à des vitesses opposées (20s / 30s).
Comment le coder en React
Mettre en place l'état et l'interval du compte à rebours
Initialise `remaining` dans useState avec les secondes totales (minutes × 60). Dans un useEffect, démarre un setInterval qui décrémente l'état de 1 chaque seconde. Retourne une fonction de nettoyage qui appelle clearInterval pour stopper le timer au démontage.
const [remaining, setRemaining] = useState(countdownMinutes * 60); useEffect(() => { const interval = setInterval(() => { setRemaining((prev) => (prev > 0 ? prev - 1 : 0)); }, 1000); return () => clearInterval(interval); }, []);Formater les unités de temps
Dérive les heures, minutes et secondes de `remaining` par division entière et modulo. Padde chaque valeur à deux caractères avec padStart pour que la largeur d'affichage reste stable quand les chiffres changent.
const hours = Math.floor(remaining / 3600); const minutes = Math.floor((remaining % 3600) / 60); const seconds = remaining % 60; const pad = (n: number) => String(n).padStart(2, "0");Animer l'entrée avec useInView
Attache une ref au conteneur extérieur et passe-la à useInView de Framer Motion avec `once: true`. Pilote `animate` sur le wrapper de contenu : quand inView, passe de opacity 0 et y 30 à l'état final. Le bloc countdown reçoit un léger scale-up avec un délai de 0.2s pour un effet de cascade.
const ref = useRef<HTMLDivElement>(null); const inView = useInView(ref, { once: true, margin: "-40px" }); <motion.div initial={{ opacity: 0, y: 30 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }} >Ajouter les engrenages de fond tournants
Place deux divs en position absolute avec aria-hidden et pointer-events:none dans la section. Entoure chaque icône Settings d'une motion.div avec `animate={{ rotate: 360 }}` (une en sens inverse avec -360). Utilise des durées différentes et une opacité globale de 0.03 pour qu'elles lisent comme une texture sans concurrencer le contenu.
<motion.div animate={{ rotate: 360 }} transition={{ duration: 20, repeat: Infinity, ease: "linear" }} aria-hidden > <Settings style={{ opacity: 0.03 }} /> </motion.div>
Quand l'utiliser
Utilise ce composant quand tu dois couper toute l'application pour une maintenance planifiée et que tu veux informer les utilisateurs du retour prévu. Il convient aux produits SaaS, outils internes et sites e-commerce pendant les déploiements ou migrations de base de données. Évite-le pour des pannes partielles ou des feature flags, un toast ou une bannière haute est moins intrusif dans ces cas. Évite aussi le compte à rebours si tu ne peux pas prédire la durée des travaux : un timer qui dépasse zéro érode la confiance plus vite qu'une absence de timer.
Utilisé par
- GitHub, Affiche une page de statut dédiée avec des heures de résolution estimées lors des incidents et maintenances planifiées.
- Shopify, Utilise des écrans de maintenance pleine page avec compteurs lors des mises à niveau majeures de la plateforme.
- Notion, Remplace l'app par une page de maintenance avec heure de retour estimée lors des interruptions planifiées.
FAQ
Comment arrêter le compte à rebours à zéro plutôt que de passer en négatif ?
La fonction de mise à jour s'en charge : `(prev) => (prev > 0 ? prev - 1 : 0)`. L'interval continue de tourner mais l'état se bloque à 0, donc l'affichage se fige à 00:00:00 sans clearInterval supplémentaire.
Peut-on persister le compte à rebours à travers les rechargements de page ?
Stocke le timestamp de fin cible (Date.now() + durée) dans localStorage au montage, puis calcule remaining depuis (endTime - Date.now()) / 1000 à chaque tick au lieu de décrémenter un compteur local. Cela survit aux rechargements et aux onglets multiples.
Pourquoi tabular-nums sur les chiffres du countdown ?
Les polices proportionnelles donnent des largeurs différentes à '1' et '8', ce qui fait bouger les cellules du countdown latéralement à chaque seconde. Appliquer font-variant-numeric: tabular-nums donne à chaque chiffre le même espace horizontal, stabilisant le layout.
Ce composant doit-il remplacer toute l'app ou se superposer ?
Pour une vraie maintenance il doit remplacer toute l'arborescence de routes, soit en définissant une variable d'environnement qui court-circuite le layout Next.js à la racine, soit en déployant une version statique de cette page sur le CDN. L'afficher par-dessus l'app charge quand même tous les bundles JS, ce qui n'a pas de sens lors d'une migration de base de données.