Cursor Morph
Un curseur personnalisé qui change de forme selon l'élément survolé : losange sur un lien, barre clignotante sur un champ de saisie, cercle par défaut. Chaque état est piloté par un attribut data-morph lu à chaque événement ; la position suit avec un lerp à 15 %.
Vous aurez accès à la démo interactive avec un compte gratuit.
Cet effet fait partie d'Effect.Labs — 811 effets vanilla, une partie gratuite, une partie premium. Curseurs comprend 26 effets, dont 1 gratuits. Découvrir la catégorie →
3 exemples d'usage



Comment ça marche
Dès que le pointeur entre dans #zone-morph, le code pose la classe cursor-active et bascule #cursorMorph de display: none à display: block. C'est la classe cursor-active — que tu dois associer à cursor: none dans ton CSS — qui masque le curseur natif dans la zone. #cursorMorph est un <div> positionné en fixed qui constitue le curseur de remplacement. À la sortie, la classe est retirée, le curseur masqué et ses classes de forme réinitialisées. Le curseur système reprend immédiatement.
À chaque mousemove, le code lit e.target.getAttribute('data-morph') — l'attribut posé sur l'élément directement sous le pointeur. Selon la valeur : la classe morph-link rend #cursorMorph en 12 × 12 px sans border-radius, tourné de 45° — un losange indigo. La classe morph-input applique 2 × 24 px arrondi à 1 px, plus une animation qui maintient l'opacité à 1 pendant la première moitié du cycle, puis la coupe à 0 avec un saut abrupt — le curseur texte natif reproduit. Sans attribut data-morph, aucune classe de morph n'est ajoutée. Avant chaque assignation, className est réinitialisé : aucune classe résiduelle ne subsiste. La transformation est instantanée — un simple échange de classes CSS.
La position suit la souris via un requestAnimationFrame et un lerp à 15 % : à chaque frame, la position courante avance de 15 % de l'écart restant. Le curseur ne colle pas exactement au pointeur — il converge rapidement avec une légère traîne perceptible. Le centrage est recalculé à chaque frame depuis offsetWidth et offsetHeight : quand la forme passe du losange 12 × 12 à la barre 2 × 24, le point d'ancrage se corrige automatiquement.
Accessibilité
- Masquer le curseur système est une décision d'accessibilité lourde. Certains utilisateurs dépendent d'un curseur OS haute visibilité — grande taille, contraste élevé, forme personnalisée via les préférences système.
cursor: noneefface toutes ces adaptations sans distinction. Limite sa portée à la zone exacte via la classecursor-activeet ne l'applique jamais sur des champs de formulaire réels ou tout élément où le curseur texte est attendu. - prefers-reduced-motion : absent du code. Deux animations coexistent : le lerp permanent via
requestAnimationFrameet l'animationcursor-blinksur l'état input. Conditionne l'initialisation de la boucle àwindow.matchMedia('(prefers-reduced-motion: reduce)')et neutralise l'animation CSS sous la media query correspondante avant tout déploiement. - L'effet est 100 % souris. Sur écran tactile,
mouseenteretmousemovene se déclenchent pas : le curseur personnalisé reste masqué, le curseur natif n'est pas affecté. Vérifie que tous les éléments marquésdata-morphrestent interactifs sans l'effet : l'attribut ne remplace ni un état:focusni un comportement clavier. - Le curseur personnalisé est un
<div>purement visuel, sans rôle ARIA ni contenu textuel. Les technologies d'assistance l'ignorent. Ne porte jamais d'information fonctionnelle par la forme du curseur : une barre clignotante n'indique pas à un lecteur d'écran qu'un champ est actif, et un losange ne se substitue pas à un état:focus-visiblesur le lien lui-même.
Compatibilité navigateur
Repose sur addEventListener, getAttribute, classList, requestAnimationFrame et offsetWidth — des APIs disponibles partout depuis des années. Le plancher réel est la syntaxe ES2015 (fonctions fléchées, IIFE) : IE 11 s'arrête sur une erreur de syntaxe avant d'entrer dans la zone. Aucun préfixe CSS requis.
Sans JavaScript, <code>#cursorMorph</code> reste en <code>display: none</code> et le curseur système s'affiche normalement — aucun contenu fonctionnel ne dépend du curseur personnalisé. Point de vigilance : si <code>cursor: none</code> est déclaré en CSS sans être conditionné à la classe <code>cursor-active</code>, le curseur natif disparaît même sans l'effet. Écris toujours la règle sur <code>.cursor-active { cursor: none; }</code>, jamais sur la zone seule.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
HTML + CSS + JS complet, prêt à coller — avec des centaines d'effets premium.
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
| Facteur de lerp (0.15 dans le JS) | 0.15 | Règle la traîne. 0.05 = arrivée nettement en retard. 0.30 = presque collé. 1.0 = suivi instantané. Sur de petites cibles, monte à 0.20–0.25 pour préserver la précision du clic. |
| Valeurs attendues par data-morph | 'button', 'link', 'input' | Le code lit getAttribute('data-morph') et ajoute la classe correspondante. Pour un nouvel état — morph-video sur une zone de lecture par exemple — ajoute la branche dans le mousemove et définis la forme en CSS. Sans attribut, le curseur prend sa forme par défaut. |
| Forme de l'état link (12 × 12 px, rotate 45°) | 12 px × 12 px, tourné de 45° | Ces valeurs CSS définissent le losange. Augmente la taille pour un losange plus grand, retire la rotation pour un carré droit. offsetWidth/offsetHeight est relu à chaque frame : modifier les dimensions en CSS ne touche pas au JavaScript. |
| Forme de l'état input (2 × 24 px, border-radius 1 px) | 2 px × 24 px | Ces dimensions imitent le curseur texte natif. Augmente la hauteur sur de grandes zones de texte ; épaissit à 4–6 px pour un curseur stylisé. Le recalcul de offsetWidth/offsetHeight garantit le centrage quelle que soit la géométrie choisie. |
| Couleur partagée (background: #6366f1) | #6366f1 (indigo) | Les trois classes de morph déclarent chacune leur couleur dans leur règle background. Pour un réglage centralisé, remplace les valeurs en dur par une variable CSS --cursor-color définie en racine et référencée dans chaque sélecteur de morph. |
| Animation cursor-blink (1 s, coupure abrupte à 50 %) | 1 s, opaque 0–50 %, invisible 51–100 % | Les keyframes coupent l'opacité à 0 à mi-cycle — pas de fondu progressif. Pour un clignotement plus nerveux, descends à 0.6 s ; pour un fondu, réécris les keyframes avec une interpolation linéaire. Ajoute animation: none sous prefers-reduced-motion pour neutraliser le clignotement sans toucher à la forme. |
FAQ
cursor: none à la classe cursor-active dans ton CSS — le JavaScript n'y touche pas. Sans cette règle, le curseur personnalisé se superpose au curseur système et les deux sont visibles simultanément. La séparation est intentionnelle : la portée de cursor: none reste sous ton contrôle, pas codée en dur dans le script.getAttribute('data-morph') renvoie null si l'attribut est absent. Le code réinitialise alors className à 'custom-cursor cursor-morph' sans ajouter aucune classe de morph : le curseur reprend sa forme par défaut. Cette réinitialisation se produit à chaque mousemove quelle que soit la cible — aucune classe résiduelle d'un survol précédent ne subsiste.<input> reçoit le focus et que l'utilisateur tape, mousemove cesse d'être émis si le pointeur est immobile. Le curseur personnalisé se fige à sa dernière position calculée. Assure-toi que cursor: none ne s'applique pas aux champs en production.