Curseurs✨ Premium

Cursor Expand

Un disque fixe de 22 px qui suit le pointeur à 15 % de l'écart par frame et s'ouvre en anneau translucide selon l'élément visé — 64 px sur un lien, 96 px sur un bouton — en 250 ms. La position est initialisée à l'entrée dans la zone, l'état visé est lu sur l'attribut data-expand de la cible.

JSInteractive

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

Carte de tarif — bascule mensuel/annuel et bouton d'achat — Cursor Expand exemple 1

① Carte de tarif — bascule mensuel/annuel et bouton d'achat

QuandCarte d'offre d'un SaaS : deux liens de bascule de facturation, un prix, une liste courte, un bouton d'action.
PourquoiL'anneau distingue deux natures de cible d'un coup d'œil : 64 px sur la bascule (un réglage), 96 px sur le bouton (l'engagement). Le curseur hiérarchise sans rien ajouter au texte.
RéglagesBouton en #4f46e5 → #7c3aed (6,3:1), liens #a5b4fc (8,7:1), .cursor-active a, .cursor-active button { cursor: none } pour un seul curseur.
Fin d'article — navigation entre articles et abonnement — Cursor Expand exemple 2

② Fin d'article — navigation entre articles et abonnement

QuandPied d'un billet de blog : liens vers l'article précédent et le suivant, un bouton d'inscription à la lettre.
PourquoiLa lecture terminée, le curseur guide la suite : l'anneau s'ouvre sur les liens de navigation et davantage sur le bouton, qui est l'action éditoriale principale.
RéglagesDisque blanc 18 px et anneaux blancs translucides (rgba(255,255,255,.14), bordure #fafaf9) sur fond papier sombre, ombres retirées pour un rendu éditorial.
En-tête de landing — menu et appel à l'action — Cursor Expand exemple 3

③ En-tête de landing — menu et appel à l'action

QuandBandeau supérieur d'une page produit : quatre liens de menu, une accroche, un bouton « Commencer ».
PourquoiSur un menu, l'anneau de 64 px encercle exactement un libellé et confirme la cible avant le clic ; sur le bouton, l'ouverture à 96 px signale l'action que la page attend.
RéglagesTailles réduites 18 / 56 / 80 px pour des libellés de menu compacts, facteur 0.25 (rattrapage en ~25 frames), bordure 1.5px.

Comment ça marche

La zone #zone-expand contient un .expand-btn portant data-expand="button" et un .expand-link portant data-expand="link". Le curseur est #cursorExpand (.cursor-expand) : position: fixed, 22 × 22 px, #6366f1, box-shadow: 0 0 0 4px rgba(99,102,241,.25), border-radius: 50%, pointer-events: none, z-index: 9999, display: none et transform: translate(-50%, -50%) — il est centré sur left/top par le CSS, sans soustraction de demi-taille dans le JS. Ses transitions : width et height en 250 ms cubic-bezier(.2,.8,.2,1), background, border-color et box-shadow en 250 ms.

mouseenter(e) initialise la position sur le pointeurexpandX = currentExpandX = e.clientX, idem en Y — puis ajoute cursor-active à la zone (règle cursor: none !important fournie) et affiche le disque. Mesuré : à la frame 0 le disque est déjà sous le pointeur (30, 30 pour un pointeur en 30, 30), pas de vol depuis le coin comme dans Dot Follower. mouseleave le cache et remet className = 'cursor-expand', ce qui purge l'état. mousemove mémorise la position, remonte de e.target au button ou au a le plus proche (e.target.closest('button, a'), à défaut e.target lui-même), lit son attribut data-expand, remet la classe de base puis ajoute on-button ou on-link.

Les deux états sont du CSS pur. .on-link : 64 px, fond rgba(99,102,241,.12), border: 2px solid #a5b4fc, box-shadow: 0 0 24px rgba(99,102,241,.45). .on-button : 96 px, fond rgba(99,102,241,.22), bordure #c7d2fe, ombre 0 0 32px rgba(99,102,241,.55) (valeurs mesurées). Le disque plein devient un anneau translucide qui encercle la cible. La largeur de bordure n'est pas dans la liste des transitions : elle saute de 0 à 2 px, seule sa couleur est animée.

Le déplacement passe par une boucle requestAnimationFrame permanente : currentExpandX += (expandX − currentExpandX) × 0.15, puis left/top. Mesuré à 120 Hz après un saut de 255 px : 113 px d'écart à la frame 5, 50 à la frame 10, 10 à la frame 20, moins de 1 px à la frame 39 (≈ 0,32 s ; deux fois plus en millisecondes à 60 Hz). Aucune transition CSS sur left/top, donc le lissage est celui de la seule boucle. La boucle tourne aussi hors survol (121 appels rAF/s mesurés, disque caché).

Accessibilité

  • Deux curseurs à l'écran sur les cibles : la zone reçoit cursor: none, mais les feuilles de style navigateur posent button { cursor: default } et a { cursor: pointer }, et une valeur déclarée l'emporte sur une valeur héritée. Mesuré : default sur le bouton, pointer sur le lien pendant que l'anneau est affiché — la flèche ou la main système réapparaissent au centre de l'anneau. Ajoutez .cursor-active a, .cursor-active button { cursor: none }.
  • Contraste du libellé du bouton : blanc 13,3 px (taille par défaut d'un button, aucune taille déclarée) sur le dégradé #6366f1 → #8b5cf6 = 4,5:1 puis 4,2:1 — sous 4,5:1 sur la moitié violette. Passez à #4f46e5 → #7c3aed (6,3:1 et 5,7:1). Lien #6366f1 sur #1a1a24 = 3,9:1 : #a5b4fc (8,7:1) ou #818cf8 (5,8:1). L'anneau au repos fait 3,9:1 sur la zone, au-dessus du seuil de 3:1 des composants.
  • Tactile : mesuré avec émulation tactile, un tap dans la zone affiche le disque figé et laisse cursor-active même après un tap hors zone. Conditionnez l'initialisation à matchMedia('(hover: hover) and (pointer: fine)').matches. Bouton et lien restent utilisables au doigt.
  • Clavier : Tab atteint bien le bouton et le lien (éléments natifs, outline intact), mais l'anneau ne réagit jamais au focus — il n'écoute que mousemove. Donnez aux cibles un état :focus-visible qui reproduise l'idée (par exemple outline: 2px solid #c7d2fe; outline-offset: 6px) pour que l'utilisateur clavier ait son équivalent.
  • prefers-reduced-motion absent : le glissement à 15 % par frame et l'ouverture en 250 ms sont des animations d'interaction. Sous @media (prefers-reduced-motion: reduce), retirez les transitions de .cursor-expand et mettez le facteur à 1 (disque collé, changement d'état instantané). Le div est vide : ajoutez aria-hidden="true".

Compatibilité navigateur

Requiert ES2015 (const/let, fonctions fléchées), classList, Element.closest, getAttribute, requestAnimationFrame, les variables CSS de la zone et une transition CSS sur width/height/box-shadow. Zéro dépendance.

Chrome 49+✓ Complet
Firefox 45+✓ Complet
Safari 10+✓ Complet
Edge 15+✓ Complet
Mobile iOS✓ Inactif (disque figé au tap)
Android Chrome✓ Inactif (disque figé au tap)

Sans JavaScript, le disque reste en display: none, la flèche système est conservée et le bouton comme le lien fonctionnent normalement. Sans transitions CSS, l'anneau change de taille d'un coup au passage sur une cible.

Le code

Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :

index.html — structure
<div class="cursor-zone" id="zone-expand">
  <div class="expand-zone-content">
    <button class="expand-btn" data-expand="button">Bouton</button>
    <a href="#" class="expand-link" data-expand="link" onclick="return false;">Lien cliquable</a>
  </div>
  <span class="info-text">Curseur qui s'agrandit</span>
</div>
<!-- Ce div est le curseur lui-même : le script le déplace et le fait
     apparaître. Sans lui, le script sort et l'effet ne rend rien. -->
<div class="cursor-expand" id="cursorExpand"></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
Facteur de lissage (JS — (expandX - currentExpandX) * 0.15) 0.15 Part de l'écart comblée par frame. 0.3 = disque presque collé ; 0.08 = traîne longue. Compté en frames, donc deux fois plus lent en millisecondes à 60 Hz.
Tailles (CSS — .cursor-expand 22px, .on-link 64px, .on-button 96px) 22 / 64 / 96 px Diamètres des trois états. Le translate(-50%, -50%) garde le centre en place quelle que soit la taille : aucun offset à ajuster dans le JS.
Attributs ciblés (HTML — data-expand="button|link" ; JS — expandType) button, link Ajoutez une valeur (data-expand="drag") et sa branche else if qui pose on-drag, puis la règle CSS correspondante : un état par nature de cible.
Lecture de la cible (JS — e.target.closest('button, a')) closest('button, a') || e.target Un span ou un svg placé dans le bouton remonte au bouton : l'état on-button vaut pour tout son contenu. Le sélecteur ne connaît que button et a — pour typer un div ou un label porteur de data-expand, élargissez-le à closest('[data-expand]').
Transitions (CSS — .cursor-expand transition) 250 ms, cubic-bezier(.2,.8,.2,1) Durée et courbe de l'ouverture. Ajoutez border-width .25s pour animer aussi l'apparition de la bordure, qui saute aujourd'hui de 0 à 2 px.
Couleurs des états (CSS — .on-link / .on-button background, border, box-shadow) indigo translucide, bordures #a5b4fc / #c7d2fe Habillage des anneaux. mix-blend-mode: difference avec un disque blanc inverse ce qu'il survole et reste lisible sur tout fond.
Position initiale (JS — mouseenter) e.clientX / e.clientY Déjà en place : le disque apparaît sous le pointeur. Ne supprimez pas ces deux lignes, sinon il repart de (0, 0) à chaque première entrée.

FAQ

Parce que cursor est une propriété héritée et que les navigateurs déclarent eux-mêmes cursor: default sur button et cursor: pointer sur a : une valeur déclarée sur l'élément bat le none hérité de la zone, même avec !important sur la zone. Mesuré : default sur le bouton, pointer sur le lien. Corrigez avec .cursor-active a, .cursor-active button { cursor: none } ou, plus large, .cursor-active * { cursor: none !important }.
Oui pour un button ou un a : e.target est l'élément le plus profond sous le pointeur (un span, un svg), mais le code remonte avec closest('button, a') avant de lire data-expand — l'état posé sur le bouton vaut pour tout son contenu. En revanche, closest ne cherche que ces deux balises : sur un div ou un label porteur de data-expand, un enfant survolé rend e.target lui-même, sans attribut, et l'anneau retombe à 22 px. Remplacez alors le sélecteur par closest('[data-expand]') ; la lecture qui suit ne change pas.
Juste avant </body>, hors de tout conteneur. position: fixed avec left = clientX prend la fenêtre pour repère ; un ancêtre porteur de transform, filter, perspective, will-change: transform ou contain: paint devient le bloc conteneur et décale le disque de la position de cet ancêtre. Le code livré place le div dans la boîte de démo : déplacez-le à la racine dès l'intégration.