Custom Cursor States
Un anneau de 32 px en position: fixed, ajouté au body et déplacé à chaque mousemove ; la zone masque le pointeur natif à l'entrée et le rend à la sortie, et l'attribut data-cursor-type de l'élément survolé choisit la forme — cercle de 48 px, losange rose de 24 px ou cercle vert en tirets — avec 200 ms de transition sur tout, position comprise.
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
Pour chaque zone [data-cursor-states], le script crée un div.alchemy-custom-cursor et l'ajoute à document.body — hors de la zone et hors du flux. Son CSS : position: fixed, z-index: 9999, 32 × 32 px, border: 2px solid #a78bfa, border-radius: 50%, transform: translate(−50%, −50%) pour centrer la boîte sur le point, pointer-events: none pour ne jamais intercepter le clic, display: none au repos, et transition: all .2s cubic-bezier(.23, 1, .32, 1). Cette règle et ses trois états étaient absents du code vendu jusqu'au 5 septembre 2026 : le div existait, invisible.
L'entrée et la sortie sont gérées sur la zone : mouseenter affiche l'anneau (display: block) et écrit zone.style.cursor = 'none' en ligne ; mouseleave le cache, remet cursor à vide et réinitialise la classe à alchemy-custom-cursor. Le pointeur natif n'est donc masqué que pendant le survol de la zone, et tout est restauré à la sortie — mesuré : display: none, style.cursor = '', classe de base.
À chaque mousemove, left/top reçoivent clientX/clientY — des coordonnées de fenêtre, cohérentes avec position: fixed, si bien que le défilement de la page n'introduit aucun décalage. Puis la classe est remise à sa base et e.target.closest('[data-cursor-type]') cherche l'élément typé le plus proche sous le pointeur : state-link donne un cercle de 48 px, bordure #6366f1, fond rgba(99,102,241,.1) ; state-action un carré de 24 px à border-radius: 4px tourné de 45° — un losange — bordure #ec4899 ; state-drag un cercle de 40 px en border-style: dashed, bordure #10b981. Hors de tout élément typé, l'anneau violet de 32 px reste.
Comme la transition porte sur all, elle anime aussi left/top : l'anneau rattrape le pointeur au lieu de le suivre au pixel. Mesuré après un saut de 496 px : 50 % du chemin à 24 ms, 97 % à 124 ms, arrivée vers 170 ms — un suivi souple pendant les gestes rapides, exact à l'arrêt. Le même délai lisse les changements de taille, de couleur et de rotation entre états. Deux points d'intégration : la zone .cursor-states-zone est une grille de trois colonnes en height: 100% doublé d'un min-height: 300px — c'est ce dernier qui lui donne sa hauteur, car le pourcentage vaut auto dans le conteneur vendu (min-height: 300px sans hauteur) : sans lui, la grille tombait à 92 px de haut, la hauteur de son contenu, et au-dessus comme au-dessous le pointeur natif reprenait ; et le CSS contient une règle .custom-cursor (fixe, masquée) qui appartient à Cursor Trail : aucun élément ne la porte ici.
Accessibilité
- prefers-reduced-motion absent du code : le retard de 200 ms et les morphings entre formes jouent quelle que soit la préférence. Ajoutez
@media (prefers-reduced-motion: reduce) { .alchemy-custom-cursor { transition: none } }— l'anneau colle alors au pointeur et change de forme instantanément, ce qui est le comportement attendu en mouvement réduit. - cursor: none retire le pointeur natif dans la zone : le curseur personnalisé est le seul repère de position. Il n'est jamais
display: blocksans survol, donc au clavier il n'existe pas et rien ne signale le focus. Les trois aires[data-cursor-type]sont desdivnon focalisables : faites-en de vraisaetbuttonavec un style:focus-visibleexplicite — le curseur ne reflète pas le focus. - Contraste : les libellés
rgba(255,255,255,.4)en 11,2 px mesurent 3,8:1 sur#0a0a0f, sous le seuil AA de 4,5:1 — passez àrgba(255,255,255,.6)(7,3:1). Les anneaux sont des indicateurs graphiques (seuil 3:1) : sur fond sombre tous passent (#6366f14,4:1, les autres ≥ 5,6:1). Sur une page claire, non :#a78bfa2,7:1 et#10b9812,5:1 sur blanc — prenez#7c3aed(5,7:1) et#047857(5,5:1). - Tactile : aucun
mousemove, donc aucun anneau et le pointeur natif n'est jamais masqué — l'effet est inerte sans dommage. Lecteurs d'écran : le div est vide et hors de la zone ; posezaria-hidden="true"à sa création (t.setAttribute('aria-hidden', 'true')) pour qu'il n'apparaisse jamais dans l'arbre. Sonz-index: 9999le place au-dessus de tout, boîtes de dialogue comprises. - Intégration : un élément
position: fixedse positionne par rapport à la fenêtre sauf si un ancêtre portetransform,filterouwill-change: transform— le div étant ajouté aubody, seul unbodytransformé le décalerait. La zone tient sa hauteur de sonmin-height: 300px(sonheight: 100%vautautosans parent dimensionné : 92 px mesurés sans le min-height) — gardez une hauteur explicite à l'intégration ; la réinitialisation à la sortie est bien faite par le code (curseur natif rendu, classe et affichage remis à zéro).
Compatibilité navigateur
Code ES5 (var, function) qui requiert Element.closest, classList, position: fixed et les transitions CSS. closest exclut Internet Explorer et Edge avant 15. Zéro dépendance.
Sans JavaScript, aucun div n'est créé et la zone garde le pointeur natif : trois colonnes avec icône et libellé, rien ne manque. Sans Element.closest (Edge 12-14, IE), le script lève une erreur au premier mousemove après avoir déjà masqué le pointeur natif à l'entrée — l'utilisateur perd son curseur dans la zone. Ajoutez un polyfill de closest ou testez son existence avant de poser les écouteurs.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="cursor-states-zone" data-cursor-states>
<div class="cursor-state-area" data-cursor-type="link">
<div class="state-icon">🔗</div>
<div class="state-label">Lien</div>
</div>
<div class="cursor-state-area" data-cursor-type="action">
<div class="state-icon">⚡</div>
<div class="state-label">Action</div>
</div>
<!-- … 3 au total, une par état de curseur … -->
</div>
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 |
|---|---|---|
Taille de base (CSS — .alchemy-custom-cursor) |
32 × 32 px, bordure 2 px #a78bfa | Anneau affiché hors de tout élément typé. Le translate(−50%, −50%) le garde centré quelle que soit la taille ; la bordure fait office d'indicateur, épaississez-la (3px) sur des fonds chargés. |
transition (CSS — all .2s cubic-bezier(.23, 1, .32, 1)) |
.2s sur toutes les propriétés | Règle à la fois le morphing entre états et le retard de position (mesuré : 97 % du chemin en 124 ms). Pour un suivi exact sans perdre le morphing, listez les propriétés : width .2s, height .2s, border-color .2s, background .2s, transform .2s, border-radius .2s. |
État link (CSS — .state-link) |
48 px, bordure #6366f1, fond rgba(99,102,241,.1) | Seul état avec un fond : il éclaircit légèrement ce qu'il survole. Réduisez le fond à .05 sur des visuels, ou supprimez-le pour un anneau pur. |
État action (CSS — .state-action) |
24 px, radius 4 px, rotate(45deg), bordure #ec4899 | Le losange vient de la rotation d'un carré arrondi ; retirez rotate(45deg) pour un carré, ou mettez border-radius: 50% et gardez la petite taille pour un point. La rotation est interpolée par la transition : le cercle « se tord » en losange en 200 ms. |
État drag (CSS — .state-drag) |
40 px, tirets, bordure #10b981 | border-style: dashed est la seule différence de tracé. dotted donne un pointillé ; une animation de rotation lente sur cet état seul suggère le mouvement. |
| Valeurs de data-cursor-type (HTML + CSS) | link, action, drag | Le JS ajoute state- + valeur, quelle qu'elle soit : un data-cursor-type="text" fonctionne dès qu'une règle .alchemy-custom-cursor.state-text existe (par exemple un trait vertical de 2 × 28 px). closest remonte les ancêtres : posez l'attribut sur le conteneur pour typer tout son contenu. |
z-index (CSS — 9999) |
9999 | L'anneau passe au-dessus de tout. Baissez-le sous celui de vos boîtes de dialogue si elles doivent le recouvrir, ou cachez-le à leur ouverture (le pointeur natif reste masqué tant qu'on est dans la zone). |
FAQ
transition: all .2s anime aussi left et top, réécrits à chaque mousemove. Chaque nouvelle position lance une transition de 200 ms depuis la position courante : mesuré, l'anneau parcourt 50 % d'un saut en 24 ms et 97 % en 124 ms, puis se pose vers 170 ms. C'est ce qui donne le suivi « souple » ; à l'arrêt, l'écart mesuré est de 0 px. Pour un suivi au pixel, excluez la position de la transition en listant les propriétés à animer (taille, couleur, transform, border-radius) — le morphing entre états reste, le retard disparaît.data-cursor-type="text" sur l'élément ou sur son conteneur — le JS utilise closest, donc les descendants héritent du type. Dans le CSS, une règle .alchemy-custom-cursor.state-text { width: 2px; height: 28px; border-radius: 1px; border-color: #e2e8f0 }. La classe est reconstruite à chaque mouvement (className = 'alchemy-custom-cursor' puis classList.add) : les états sont exclusifs et il n'y a rien à retirer. Si des éléments typés sont imbriqués, c'est le plus proche du pointeur qui gagne.min-height: 300px. .cursor-states-zone déclare aussi height: 100%, mais son conteneur .demo-preview n'a qu'un min-height: 300px — pas une hauteur — et un pourcentage sur un parent sans hauteur définie vaut auto : sans le min-height, la grille prenait la hauteur de son contenu, 92 px (icône, libellé, deux fois 16 px de marge intérieure), centrée dans les 300 px, et au-dessus comme au-dessous ni anneau ni cursor: none (mesuré). Pour une autre hauteur, changez le min-height de la zone ou donnez une hauteur en px à son conteneur. Au passage, la règle .custom-cursor du CSS vient d'un autre effet (Cursor Trail) et peut être supprimée.