Curseurs✨ Premium

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.

JSCSSCursor

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

Trois intentions, trois formes — en-tête et hero d'un site de studio — Custom Cursor States exemple 1

① Trois intentions, trois formes — en-tête et hero d'un site de studio

QuandPage d'accueil d'un studio où cohabitent un lien vers les projets, un bouton de prise de contact et un bandeau à faire glisser.
PourquoiLe curseur annonce ce que le clic fera avant le clic : cercle qui grandit sur le lien, losange sur l'action, cercle en tirets sur ce qui se déplace. Trois formes suffisent, et la transition de 200 ms rend les passages d'une zone à l'autre lisibles sans à-coup.
RéglagesBordures épaissies à 3px pour rester visibles sur les visuels, state-link à 56 px, cursor: none conservé — tout le hero est la zone, le pointeur natif ne revient qu'au-dessus de la navigation.
Kanban — glisser une carte, ouvrir un ticket, archiver — Custom Cursor States exemple 2

② Kanban — glisser une carte, ouvrir un ticket, archiver

QuandTableau à trois colonnes d'un outil de gestion : cartes déplaçables, liens vers les tickets, bouton d'archivage.
PourquoiDans une interface dense, le losange et le cercle en tirets distinguent immédiatement « ceci se clique » de « ceci se déplace » : le curseur devient une légende. Les aires typées sont ici les colonnes elles-mêmes, pour que la forme change dès l'entrée dans la colonne.
RéglagesAnneau de base réduit à 24 px et transition: .12s pour un suivi plus serré dans les listes ; state-drag teinté de la couleur de la colonne « À faire ».
Galerie — faire défiler, voir le projet, télécharger — Custom Cursor States exemple 3

③ Galerie — faire défiler, voir le projet, télécharger

QuandGrille de vignettes d'un portfolio : une piste d'images à faire défiler, une fiche projet, un dossier à télécharger.
PourquoiSur des visuels pleins, un pointeur natif se perd ; un anneau de 40 à 48 px reste visible et sa forme dit l'action. Le fond translucide de state-link (rgba(99,102,241,.1)) fait office de loupe légère sur l'image.
Réglagesstate-link porté à 64 px avec fond .15, state-action gardé petit (24 px) pour ne pas masquer le libellé du bouton, courbe cubic-bezier(.23, 1, .32, 1) conservée.

Comment ça marche

Pour chaque zone [data-cursor-states], le script crée un div.alchemy-custom-cursor et l'ajoute à document.bodyhors 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: block sans survol, donc au clavier il n'existe pas et rien ne signale le focus. Les trois aires [data-cursor-type] sont des div non focalisables : faites-en de vrais a et button avec un style :focus-visible explicite — 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 (#6366f1 4,4:1, les autres ≥ 5,6:1). Sur une page claire, non : #a78bfa 2,7:1 et #10b981 2,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 ; posez aria-hidden="true" à sa création (t.setAttribute('aria-hidden', 'true')) pour qu'il n'apparaisse jamais dans l'arbre. Son z-index: 9999 le place au-dessus de tout, boîtes de dialogue comprises.
  • Intégration : un élément position: fixed se positionne par rapport à la fenêtre sauf si un ancêtre porte transform, filter ou will-change: transform — le div étant ajouté au body, seul un body transformé le décalerait. La zone tient sa hauteur de son min-height: 300px (son height: 100% vaut auto sans 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.

Chrome 41+✓ Complet
Firefox 35+✓ Complet
Safari 9+✓ Complet
Edge 15+✓ Complet
Mobile iOS✓ Curseur natif (pas de survol)
Android Chrome✓ Curseur natif (pas de survol)

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) :

index.html — structure
<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>
🔒 Débloquer le code complet — à partir de 2,99 € le 1er mois

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éfautEffet
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

Parce que 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.
Deux lignes. Dans le HTML, 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.
De son 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.