Parent : [[INDEX-WEB-UIUX]]
Web — UI patterns extraits de ProjectBeta : gradient button, custom cursor, CSS design system
Pattern 1 — Gradient button animé sans hydratation mismatch (SSR safe)
Problème : Utiliser Math.random() au render initial cause un hydration mismatch Next.js entre le serveur et le client.
Solution : Initialiser avec des positions statiques { x: 0, y: 0 } pour le SSR. Dans useEffect (client only), setter isMounted = true puis remplacer par les positions aléatoires réelles. Désynchroniser les boutons avec un initialDelay aléatoire pour qu'ils ne clignotent pas tous ensemble.
const [colorPositions, setColorPositions] = useState(staticPositions) // SSR safe
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
setColorPositions(getInitialPositions()) // Positions aléatoires côté client
const initialDelay = Math.random() * 2000 // Désynchroniser les boutons
const timeout = setTimeout(startAnimation, initialDelay)
return () => clearTimeout(timeout)
}, [])
Pattern 2 — Gradient animé : RAF throttlé (update toutes les 2s)
Problème : Animer des gradients complexes à 60fps via requestAnimationFrame consomme trop de CPU.
Solution : Maintenir le RAF pour garder une boucle smooth, mais ne mettre à jour les positions qu'une fois toutes les 2000ms. willChange: 'transform' + transform: translate3d() pour forcer l'accélération GPU.
// Dans la boucle RAF :
if (elapsed >= 2000) {
setColorPositions(getInitialPositions())
animationStartTime = now
}
// Sur chaque élément gradient :
style={{ transform: `translate3d(${x}%, ${y}%, 0)`, willChange: 'transform' }}
Pattern 3 — Custom cursor avec particules : throttle à 30ms
Problème : Générer une particule à chaque mousemove (60fps) crée des milliers de particules par seconde et sature le state React.
Solution : Throttler la génération de particules à 30ms minimum entre chaque création. Utiliser Date.now() avec useRef (pas de state) pour tracker le dernier temps sans re-render.
const lastParticleTimeRef = useRef(0)
if (now - lastParticleTimeRef.current > 30) {
lastParticleTimeRef.current = now
// créer la particule
}
Pattern 4 — Custom cursor rendu via createPortal
Problème : Un cursor custom dans le DOM normal est affecté par les overflow: hidden des parents et les stacking contexts.
Solution : Utiliser createPortal(cursorJSX, document.body) pour rendre le cursor directement dans le body, hors de tout contexte. pointerEvents: 'none' + zIndex: 99999 pour ne jamais bloquer les interactions.
return createPortal(
<div style={{ position: 'fixed', pointerEvents: 'none', zIndex: 99999 }}>
{/* cursor + particules */}
</div>,
document.body
)
Pattern 5 — CSS Variables pour le design system dark/light
Problème : Hardcoder des couleurs dans le CSS rend le toggle dark/light mode impossible sans réécrire tous les styles.
Solution : Définir toutes les couleurs en variables CSS HSL dans :root (light) et .dark (dark). Référencer uniquement via hsl(var(--primary)) dans Tailwind. Les composants n'ont jamais de couleurs hardcodées.
:root {
--primary: 262 83% 58%;
--background: 0 0% 100%;
}
.dark {
--primary: 262 83% 58%;
--background: 240 10% 3.9%;
}
// tailwind.config.ts
primary: { DEFAULT: 'hsl(var(--primary))' }
Pattern 6 — Masquer les scrollbars globalement sans casser le scroll
Problème : overflow: hidden cache le scroll, mais scrollbar-width: none le garde fonctionnel sans afficher la barre.
Solution : Appliquer sur * avec les 3 propriétés pour cross-browser coverage.
* {
scrollbar-width: none !important; /* Firefox */
-ms-overflow-style: none !important; /* IE/Edge */
}
*::-webkit-scrollbar { display: none !important; } /* Chrome/Safari */
Pattern 7 — Autocomplete browser sans le fond bleu horrible
Problème : Les navigateurs appliquent un fond bleu sur les champs autofill, ignorant le dark theme.
Solution : Override via -webkit-box-shadow inset blanc — le seul moyen de forcer la couleur de fond sur autofill.
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
-webkit-text-fill-color: #ffffff !important;
-webkit-box-shadow: 0 0 0px 1000px #1a1a1a inset !important;
}
Pattern 8 — CSS animations keyframes pour effets de texte
Problème : Implémenter des effets texte (rainbow, fire, glitch, sparkle) avec JS génère des re-renders constants.
Solution : Définir tous les effets en @keyframes CSS pur. Appliquer via animation property. Zéro JS, zéro re-render, GPU accelerated nativement.
@keyframes rainbowShift {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
@keyframes glitch {
20% { transform: translate(-2px, 2px); }
40% { transform: translate(-2px, -2px); }
60% { transform: translate(2px, 2px); }
}
Anti-pattern — Scroll listener sans passive + sans RAF
Problème : Un scroll listener qui fait des DOM reads/writes directement bloque le thread principal et cause du jank visible.
Solution : { passive: true } sur le listener. Debouncer via requestAnimationFrame avec un flag isScheduled pour éviter les RAF multiples simultanés.
window.addEventListener('scroll', handleScroll, { passive: true })
// Dans handleScroll :
if (isScheduled) return
isScheduled = true
rafId = requestAnimationFrame(() => {
isScheduled = false
// DOM reads ici
})