Custom Circle Cursor
Un cercle substitué au curseur natif dans une zone délimitée : `mouseenter` l'affiche et efface le pointeur système, `mousemove` le téléporte à la coordonnée exacte du pointeur — zéro interpolation, zéro délai — et teste à chaque événement si la cible porte l'attribut `data-hover` pour basculer l'état visuel du cercle.
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
Trois écouteurs sont posés sur le conteneur #zone-circle, jamais sur le cercle lui-même. mouseenter déclenche deux actions : il ajoute la classe cursor-active à la zone — ce qui active la règle CSS cursor: none et efface le pointeur système — puis passe le cercle de display: none à display: block. mouseleave inverse tout : retire cursor-active, remet le cercle à display: none et supprime hover pour un état propre à la prochaine entrée.
À chaque mousemove, le cercle reçoit deux valeurs : style.left = e.clientX + 'px' et style.top = e.clientY + 'px'. Ce sont des coordonnées viewport écrites directement, sans coefficient, sans requestAnimationFrame, sans lerp. Le cercle ne suit pas le curseur avec délai ou traîne — il occupe la même position que lui à chaque instant. Le centrage au point de contact est géré par transform: translate(-50%, -50%) côté CSS. La propriété mix-blend-mode déclarée sur le cercle fusionne visuellement le cercle avec le contenu en dessous (inversion, soustraction, multiplication) — sans calcul JS.
Toujours dans le mousemove, après repositionnement, le code inspecte e.target : si cet élément possède l'attribut data-hover, la classe hover est ajoutée au cercle ; sinon, elle est retirée. Ce contrôle s'exécute à chaque événement, sans cache ni délai — le basculement est instantané. Le JS ne connaît pas l'apparence du cercle : il fournit un état binaire que le CSS interprète visuellement. L'attribut data-hover est ton sélecteur sémantique — tu désignes les éléments actifs dans la zone sans toucher au JS.
Accessibilité
- Masquer le curseur natif est une décision d'accessibilité lourde, pas une simple animation. Si le cercle custom ne se charge pas (JS bloqué, erreur de rendu),
cursor: noneprive l'utilisateur de son repère sans rien lui substituer. Vérifie que#cursorCircleest présent dans le DOM avant de démarrer les écouteurs. - Sur clavier,
mousemovene se déclenche jamais : l'utilisateur ne voit pas le cercle, et sicursor: nones'applique via une autre règle, le repère visuel disparaît sans substitut. Assure-toi que chaque élément cliquable dans la zone dispose d'un état:focus-visibleexplicite — anneau de focus, ombre — indépendant de l'effet curseur. - Sur écran tactile, aucun événement
mouse*ne circule : le cercle reste àdisplay: none, la zone conserve son curseur natif. Repli propre, aucun code à écrire. Pour bloquer lesmousemoveémulés au tap, conditionne les écouteurs àmatchMedia('(hover: hover) and (pointer: fine)'). - Le cercle est un ornement purement visuel. Assure-toi qu'il porte
pointer-events: noneen CSS, sinon il intercepte les événements mouse* à sa propre position et casse la détection dedata-hoversur les éléments en dessous. Ne porte aucune information par le cercle ou son étathoverseul — une partie de ton audience ne verra jamais ni l'un ni l'autre.
Compatibilité navigateur
Toutes les APIs utilisées sont ES5-compatibles : getElementById, classList, addEventListener, style.left/top/display, hasAttribute. Le seul plancher est syntaxique : les fonctions fléchées (ES2015) bloquent IE 11. Côté CSS, mix-blend-mode est le critère limitant — absent de IE et Edge pré-Chromium, où le cercle s'affiche sans fusion.
Sans JavaScript, le cercle n'est jamais rendu visible et <code>cursor-active</code> n'est jamais ajouté : le pointeur natif reste intact. Sans <code>mix-blend-mode</code> (Edge 18 et antérieur), le cercle s'affiche en aplat opaque — un repli fonctionnel si tu lui assignes une couleur de fond lisible. Aucun code de secours à écrire dans les deux cas.
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 |
|---|---|---|
| Zone d'activation (#zone-circle) | le conteneur de 300 px de haut dans la démo | Les trois écouteurs (mouseenter, mouseleave, mousemove) sont posés sur cet élément. Remplace l'id par n'importe quel conteneur ou par document.body pour un curseur pleine page. |
| Taille du cercle (CSS width/height sur #cursorCircle) | définie en CSS, non codée en dur dans le JS | Le JavaScript n'écrit que left et top — il ignore la taille du cercle. Modifie librement width et height en CSS, le centrage reste assuré par transform: translate(-50%, -50%). |
| mix-blend-mode (CSS sur #cursorCircle) | défini en CSS — 'difference' dans la démo | difference inverse les couleurs sous le cercle. exclusion donne un résultat similaire mais plus doux. multiply assombrit (fond clair). screen éclaircit (fond sombre). normal désactive la fusion pour un aplat opaque — préférable dans les interfaces de travail. |
| L'attribut data-hover | présent sur le bouton de la démo (data-hover="true") | À chaque mousemove, le code teste e.target.hasAttribute('data-hover') — seule la présence compte, pas la valeur. Pose-le sur n'importe quel élément HTML (lien, image, SVG, bouton) pour déclencher hover sur le cercle. |
| La classe .hover sur #cursorCircle (CSS) | basculée par data-hover — apparence entièrement à définir en CSS | Le JS ajoute ou retire .hover ; tout le rendu visuel dépend du CSS que tu écris pour ce sélecteur. Exemples : scale(1.6) pour agrandir le cercle au-dessus des éléments actifs ; changement de mix-blend-mode selon l'état ; réduction d'opacité pour un effet d'effacement. |
| La classe .cursor-active (CSS sur #zone-circle) | ajoutée par mouseenter, retirée par mouseleave | C'est cette classe qui doit porter cursor: none dans ton CSS — le JS ne touche jamais directement à la propriété curseur. Si tu veux masquer le pointeur natif hors zone également, déplace cursor: none sur le body et réserve la classe à d'autres styles de zone. |
FAQ
cursor-active au conteneur #zone-circle dès que le pointeur entre dedans. Une règle CSS sur ce sélecteur déclare cursor: none — le JS ne touche jamais la propriété directement. Au mouseleave, la classe est retirée et le curseur natif reprend ses droits immédiatement. Sans JavaScript, cursor-active n'est jamais ajouté et le pointeur reste visible.style.left = e.clientX + 'px' et style.top = e.clientY + 'px' directement à chaque mousemove, sans coefficient ni interpolation. La position du cercle est identique à celle du pointeur à chaque instant — c'est une substitution géométrique, pas un suivi avec friction. Pour une traîne, un lerp par frame ou une transition CSS sur left/top — c'est un effet différent.mouseenter et mousemove ne circulent pas : le cercle reste à display: none, cursor-active n'est jamais ajouté et le curseur natif n'est jamais masqué. Aucun code de désactivation à écrire. Pour bloquer les mousemove émulés au tap, conditionne les écouteurs à matchMedia('(hover: hover) and (pointer: fine)').