Curseurs✨ Premium

Gravity Well Cursor

Soixante points de 3 à 8 px, teintes de 220° à 300°, chacun rappelé vers sa position d'origine à 3 % par image ; dans un rayon de 30 % de la zone autour du pointeur, une poussée jusqu'à 3 % par image les écarte. Le code repousse, il n'attire pas — et sa boucle requestAnimationFrame ne s'arrête plus après la première entrée.

JSPhysicsrequestAnimationFrame

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

Les menaces n'approchent pas — hero d'une offre de cybersécurité — Gravity Well Cursor exemple 1

① Les menaces n'approchent pas — hero d'une offre de cybersécurité

QuandPage d'accueil d'un produit de sécurité : accroche, sous-titre, bouton, sur un champ de points qui occupe tout l'écran.
PourquoiUn champ qui s'écarte du pointeur illustre littéralement le message — ce qui approche est repoussé. La répulsion, ici, est la bonne métaphore ; une attraction dirait le contraire. Le texte, au-dessus des particules, reste lisible : elles font 3 à 8 px.
RéglagesRayon du champ porté à 40, poussée 4, rappel .02 pour un retour plus lent, teintes resserrées 200 + 40 × Math.random() (bleus) ; le texte en z-index: 1 au-dessus des points.
Champ de particules — widget d'un tableau de bord scientifique — Gravity Well Cursor exemple 2

② Champ de particules — widget d'un tableau de bord scientifique

QuandCarte de tableau de bord ou de laboratoire : en-tête, légende, et une simulation dans le corps de la carte.
PourquoiDans une interface d'analyse, un champ que le pointeur perturbe puis qui revient au repos en 1,6 s se lit comme une simulation vivante, sans vidéo. Le crosshair natif conservé convient à un outil. La boucle permanente doit ici être arrêtée au repos : la carte reste affichée longtemps.
Réglages100 particules, tailles 2 + 3 × Math.random(), palette monochrome hsl(190, 80%, 60%) ; arrêt de la boucle 2 s après la sortie du pointeur (cf. FAQ).
Affiche de festival électro — fond réactif — Gravity Well Cursor exemple 3

③ Affiche de festival électro — fond réactif

QuandVisuel plein cadre d'un festival : nom, dates, têtes d'affiche, sur un fond de points colorés.
PourquoiLes teintes de 220° à 300° du code — bleu, violet, magenta — sont celles de l'électro et des lasers ; les points qui fuient la main donnent au poster une réaction physique sans animation permanente visible. Le dégradé radial d'origine fait un fond de scène.
Réglages80 particules, tailles 3 + 7 × Math.random() avec halo doublé (0 0 3·taille), rayon 25 et poussée 5 pour des fuites brèves et vives, rappel .05.

Comment ça marche

Pour chaque zone [data-gravity-well], le script crée 60 div.gravity-particle : diamètre 3 + 5 × Math.random() (3 à 8 px), teinte 220 + 80 × Math.random() appliquée en hsl(h, 70%, 65%) — du bleu au magenta —, halo box-shadow: 0 0 2·taille px hsla(h, 70%, 65%, .4), et une position d'origine tirée au hasard, écrite en left/top en pourcentage de la zone. Chaque particule garde en mémoire homeX/homeY (l'origine) et x/y (la position courante). Le CSS de la particule : position: absolute, ronde, transition: none. La zone : 100% × 300px, dégradé radial #12122e → #0a0a1a, cursor: crosshair, position: relative, overflow: hidden. Jusqu'au 5 septembre 2026, le HTML vendu contenait en plus 60 particules figées : 120 points, la moitié immobiles ; il n'en reste que les 60 du script.

La boucle d(), en requestAnimationFrame, traite chaque particule en deux temps. D'abord la poussée : le pointeur est converti en pourcentages de la zone (a, n) à chaque mousemove, et la distance h entre la particule et le pointeur est calculée dans ces unités. Si h < 30 et que le pointeur est dans la zone (a > −100), la particule reçoit un déplacement de 3 × (1 − h / 30) unités le long de l'angle atan2 plus π — c'est-à-dire à l'opposé du pointeur : 3 % de la zone par image au contact, rien à 30 unités. Ensuite le rappel : x += .03 × (homeX − x), 3 % de l'écart restant par image, soit 95 % du retour en une centaine d'images — environ 1,6 s à 60 images par seconde (mesuré : à 0,03 px de l'origine deux secondes après la sortie du pointeur).

Mesuré dans Chromium, pointeur immobile au centre : 24 particules déplacées de plus de 2 px, la plus éloignée de 61 px, et toutes s'éloignent du pointeur — c'est une répulsion. Le nom et l'ancienne description promettaient une attraction ; la description du catalogue a été corrigée (« les particules proches du curseur sont repoussées »), le code n'a pas changé. Conséquence des unités en pourcentage : le champ de 30 unités mesure 30 % de la largeur sur 30 % de la hauteur, une ellipse de 223 × 90 px dans une zone de 744 × 300 px — deux fois et demie plus large que haute. Sur une zone carrée, le champ serait rond.

Le cycle de vie est le point faible. mouseenter lance la boucle une seule fois (drapeau o) ; mouseleave se contente d'envoyer le pointeur à −1000, ce qui coupe la poussée et laisse les particules rentrer — mais rien n'arrête jamais la boucle. Mesuré : une position forcée à la main est réécrite 120 ms après la sortie du pointeur, et le sera encore une heure plus tard. Chaque image coûte 60 écritures de left/top (donc une mise en page, malgré le will-change: transform déclaré sur des éléments dont le transform ne change jamais) et un appel getBoundingClientRect() dont le résultat n'est pas utilisé.

Accessibilité

  • prefers-reduced-motion absent du code : soixante points s'écartent du pointeur et une boucle tourne en permanence, quelle que soit la préférence. Aucune règle CSS ne peut l'arrêter (positions écrites en ligne, transition: none) : conditionnez l'initialisation entière à !matchMedia('(prefers-reduced-motion: reduce)').matches, ou créez les particules sans lancer la boucle — un champ d'étoiles immobile est un repli honnête.
  • L'effet est exclusivement souris : sans mousemove, les particules restent à leur origine. Le pointeur natif reste visible (cursor: crosshair, pas cursor: none) et la zone est un div non focalisable : gardez-la décorative, aucune information ne doit dépendre du champ.
  • Contraste : les particules sont des éléments graphiques décoratifs (seuil 3:1). Sur le dégradé #12122e → #0a0a1a, la teinte la moins visible (autour de 260°) mesure 4,0:1 et la plus visible 6,4:1 — toutes passent. Sur une page claire, non : 2,9:1 au minimum sur blanc ; baissez la luminosité de 65% à 55% dans la chaîne hsl(). Le code ne contient aucun texte.
  • Lecteurs d'écran : 60 div vides, muets, créés une fois au chargement — l'arbre d'accessibilité n'est ensuite plus touché, seuls les styles changent. Posez tout de même aria-hidden="true" sur la zone [data-gravity-well].
  • Intégration : la zone doit garder position: relative (repère des pourcentages), overflow: hidden (les particules poussées vers un bord en sortent) et une hauteur explicite — 300px dans le code vendu. Comme les positions sont en pourcentage, le champ suit les proportions de la zone (ellipse sur une zone large). Prévoyez l'arrêt de la boucle : dans mouseleave, après 2 s, ou dès que toutes les particules sont à moins de 0,1 % de leur origine, mettez le drapeau à faux et sortez de d().

Compatibilité navigateur

Code ES5 (var, function) : il requiert requestAnimationFrame, getBoundingClientRect, les couleurs hsl()/hsla() et box-shadow. Zéro dépendance.

Chrome 24+✓ Complet
Firefox 23+✓ Complet
Safari 6.1+✓ Complet
Edge 12+✓ Complet
Mobile iOS✓ Statique (points immobiles)
Android Chrome✓ Statique (points immobiles)

Sans JavaScript, la zone est un simple dégradé radial : aucune particule, puisque c'est le script qui les crée. Sans requestAnimationFrame (IE 9), les 60 points sont créés à leur origine et n'en bougent plus.

Le code

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

index.html — structure
<div class="gravity-zone" data-gravity-well></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
Nombre de particules (JS — h < 60) 60 Points créés au chargement, chacun avec sa boucle de mise à jour. Le coût est linéaire : 60 écritures de position par image. Au-delà de 150, passez les positions en transform: translate() pour éviter la mise en page.
Rayon du champ (JS — h < 30 et 1 − h / 30) 30 unités (30 % de la largeur × 30 % de la hauteur) Portée de la répulsion, en pourcentage de chaque dimension : une ellipse sur une zone large. Changez les deux occurrences ensemble. 15 = trou étroit sous le pointeur ; 50 = presque toute la zone réagit.
Force de poussée (JS — 3 × (1 − h / 30)) 3 % de la zone par image au contact Déplacement maximal par image, dégressif jusqu'au bord du champ. 1 : les points s'écartent doucement et le rappel les retient près de l'origine ; 6 : ils sont projetés loin puis reviennent lentement.
Rappel vers l'origine (JS — .03 × (homeX − x)) .03 par image Fraction de l'écart parcourue à chaque image : 95 % du retour en ~100 images (1,6 s). .1 = retour en 0,5 s, champ plus « rigide » ; .01 = points qui flottent plusieurs secondes.
Tailles (JS — 3 + 5 × Math.random()) 3 à 8 px Diamètre des points ; le halo suit (0 0 2·taille). Des points de 1 à 3 px donnent un ciel étoilé ; de 6 à 14 px, des bulles.
Couleurs (JS — 220 + 80 × Math.random(), hsl(h, 70%, 65%)) bleu → magenta, 70 % / 65 % Plage de teinte et saturation/luminosité fixes. hsl(h, 70%, 55%) pour tenir 3:1 sur un fond clair ; une teinte constante pour un champ monochrome ; le halo reprend la même teinte à 40 % d'opacité.
Arrêt de la boucle (JS — à ajouter) jamais (boucle permanente) Dans d(), sortez sans redemander d'image quand le pointeur est parti et que chaque particule est à moins de 0,1 unité de son origine, et remettez o à faux pour que le prochain mouseenter relance. Le champ devient gratuit au repos.

FAQ

Il repousse. Le déplacement est appliqué le long de Math.atan2(dy, dx) + Math.PI : l'angle vers le pointeur, retourné de 180°. Mesuré pointeur immobile au centre, 24 particules bougent, la plus éloignée de 61 px, et toutes finissent plus loin du pointeur qu'au départ. La description du catalogue a été mise en accord avec le code (« repoussées ») ; le nom, lui, hérite de l'intention initiale. Pour obtenir une vraie attraction, retirez le + Math.PI — mais sans amortissement, les points se ruent sur le pointeur, le traversent et oscillent autour : ajoutez une distance minimale (h > 3) ou réduisez la poussée à 1 pour qu'ils s'agglutinent sans trembler.
Parce que toutes les distances sont calculées en pourcentages de la zone, pas en pixels. Le pointeur est converti en (clientX − left) / width × 100 et (clientY − top) / height × 100, les particules vivent en left/top en %, et le rayon de 30 s'applique à ces unités : 30 % de 744 px horizontalement (223 px), 30 % de 300 px verticalement (90 px). Le champ est donc une ellipse aux proportions de la zone. C'est ce qui rend le code indépendant de la taille — mais si vous voulez un champ rond, convertissez en pixels avant de mesurer la distance, ou donnez à la zone un format carré.
Oui, indéfiniment. mouseenter lance d() une fois et met le drapeau o à vrai ; mouseleave ne fait que déplacer le pointeur virtuel à −1000, ce qui coupe la poussée. La boucle continue de rappeler des particules déjà à l'origine et de réécrire 60 positions par image — mesuré : une position forcée est réécrite 120 ms après la sortie, et le serait encore des heures plus tard. Sur une page qui reste ouverte, c'est un travail permanent et inutile. Le correctif tient en trois lignes : dans d(), si le pointeur est à −1000 et que chaque particule est à moins de 0,1 unité de son origine, positionnez-la exactement, mettez o = false et ne redemandez pas d'image ; le prochain mouseenter relancera la boucle.