Cursor Text Reveal
Un titre de 32 px réduit à un contour blanc à 30 %, doublé par un ::before en texte plein que clip-path: circle(50px) découpe à la position du pointeur (transition 100 ms) : le mot n'apparaît que sous le cercle et disparaît à la sortie, le cercle étant renvoyé à −100 px.
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
La zone #zone-text-reveal (fond #0a0a0f en style inline) contient .cursor-reveal-container avec l'attribut data-text="REVEAL ME" et le même texte comme contenu. Le conteneur porte le texte « fantôme » : font-size: 2rem, font-weight: 800, color: transparent, -webkit-text-stroke: 1px rgba(255,255,255,.3), user-select: none. Son ::before porte le texte plein : content: attr(data-text), position: absolute; inset: 0, color: #fff, -webkit-text-stroke: 0, et surtout clip-path: circle(50px at var(--cursor-x, -100px) var(--cursor-y, -100px)) avec transition: clip-path .1s.
Le JS écoute mousemove sur la zone, pas sur le texte, mais calcule les coordonnées par rapport au conteneur de texte : rect = textRevealContainer.getBoundingClientRect(), x = clientX − rect.left, puis setProperty('--cursor-x', x + 'px') sur le conteneur. mouseleave renvoie les deux variables à -100px. Mesuré : clip-path: circle(50px at 19px 18.5px) pendant le survol, circle(50px at -100px -100px) après la sortie — le cercle est alors entièrement hors de la boîte du texte, plus rien de plein n'est visible.
Deux calques du même mot, superposés au pixel : le contour est le vrai texte, le plein est le pseudo-élément. inset: 0 lui donne la boîte exacte du conteneur, et comme il hérite de la police, de la graisse et de la taille, les glyphes tombent aux mêmes positions. Condition absolue : data-text doit être identique au contenu, sinon les deux textes se désalignent. La transition de 100 ms sur clip-path fonctionne parce que les deux extrémités sont des circle() de même rayon : le navigateur interpole le centre.
La zone écoutée, .cursor-zone, pose width: 100% et 300 px de haut : elle remplit son parent, et l'écouteur couvre toute cette surface, pas seulement la colonne du texte (186 px de large) — les coordonnées, elles, restent calculées par rapport au conteneur de texte. Pas de cursor: none, pas de requestAnimationFrame (0 appel/s mesuré) : la flèche système reste, le cercle est collé au pointeur, le seul lissage est la transition CSS.
Accessibilité
- Le titre est lu deux fois : le contenu généré par CSS est exposé aux technologies d'assistance, et l'arbre d'accessibilité de Chromium montre deux nœuds
StaticText« REVEAL ME » — le vrai texte et le::before. Utilisez la syntaxe de texte alternatifcontent: attr(data-text) / ""(Chrome 77+, Safari 17.4+, Firefox 128+) pour que le pseudo-élément soit ignoré par les lecteurs d'écran. - Contraste au repos : le contour de 1 px à
rgba(255,255,255,.3)compose#545457sur#0a0a0f, 2,6:1, et il est creux — le mot est illisible sans survol, par construction. Le texte révélé fait 19,8:1. Si le titre porte du sens, montez le contour àrgba(255,255,255,.5)(5,3:1) ou affichez le plein sans survol via@media (hover: none)et(prefers-reduced-motion: reduce):clip-path: none. - Tactile : un tap émule un
mousemovepuis, au tap suivant hors zone, unmouseleave— l'utilisateur voit un cercle de 50 px figé au point touché, jamais le mot entier. Clavier : rien ne se révèle. D'où l'intérêt du repli@media (hover: none) { .cursor-reveal-container::before { clip-path: none } }. user-select: noneempêche de sélectionner et copier le titre, même révélé ; retirez la déclaration si le texte doit rester copiable. Le pseudo-élément, lui, n'est jamais sélectionnable — c'est le contenu réel qui compte pour la copie.- Zoom et reflow :
font-size: 2remsuit le zoom du navigateur, mais le rayon de 50 px et les positions en pixels ne suivent pas — à 200 %, le cercle couvre deux fois moins de texte. Exprimez le rayon enem(circle(1.6em …)) pour qu'il grandisse avec la police.
Compatibilité navigateur
Requiert clip-path: circle() non préfixé avec interpolation, -webkit-text-stroke (supporté sous ce nom par tous, Firefox 49+ inclus), la propriété raccourcie inset et les variables CSS. JS ES2015. Zéro dépendance.
Sans clip-path (navigateurs antérieurs à 2017), le texte plein s'affiche en entier et en permanence : un titre blanc ordinaire, lisible. Sans JavaScript, les variables restent à −100 px : seul le contour est visible, rien ne se révèle. Sans inset (Safari avant 14.1, Firefox avant 66), le pseudo-élément perd sa boîte et se place en haut à gauche du conteneur, désaligné du contour.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="cursor-zone" id="zone-text-reveal" style="background: #0a0a0f;">
<div class="cursor-reveal-container" data-text="REVEAL ME" id="textRevealContainer">REVEAL ME</div>
<span class="info-text" style="color: rgba(255,255,255,0.5);">Revelation du texte</span>
</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 |
|---|---|---|
Rayon du cercle (CSS — circle(50px …)) |
50 px | Surface révélée autour du pointeur. À 32 px de texte, 50 px dévoilent deux ou trois lettres ; 120px révèle un mot entier. En em, le rayon suit la taille de police. |
Contour (CSS — -webkit-text-stroke: 1px rgba(255,255,255,.3)) |
1 px à 30 % | Visibilité du texte fantôme. .5 = 5,3:1, lisible au repos ; 0 = texte invisible jusqu'au survol (le plein devient la seule trace). |
Transition (CSS — ::before clip-path .1s) |
100 ms | Retard du cercle sur le pointeur. .3s donne une traîne visible ; 0 colle le cercle. Interpolable uniquement entre deux circle() de même forme. |
Texte (HTML — contenu + data-text) |
REVEAL ME | Les deux doivent rester identiques, y compris espaces et casse ; sinon contour et plein se désalignent. Pour un texte traduit, changez les deux. |
Taille et poids (CSS — font-size: 2rem; font-weight: 800) |
32 px, 800 | Le ::before hérite des deux : changez-les sur le conteneur seulement. Une graisse forte est nécessaire pour qu'un contour de 1 px reste lisible. |
Position de repos (JS — '-100px' dans mouseleave ; CSS — replis var(--cursor-x, -100px)) |
−100 px | Où part le cercle quand la souris sort. Pour un texte de plus de 100 px de haut, éloignez davantage (-200px) pour que le cercle sorte tout à fait de la boîte. |
Couleur du plein (CSS — ::before color) |
#fff | Couleur révélée. Un dégradé se pose avec background-clip: text sur le pseudo-élément ; la découpe clip-path s'applique par-dessus sans conflit. |
FAQ
data-text : le ::before affiche attr(data-text), le contour affiche le contenu HTML ; si l'un a été modifié sans l'autre (traduction, espace, casse), les deux mots ne coïncident plus. Deuxième cause : une largeur différente qui change le retour à la ligne — le pseudo-élément prend la boîte du conteneur via inset: 0, donc fixez la largeur du conteneur si le texte s'étale sur plusieurs lignes. Troisième : un padding sur le conteneur, que inset: 0 ignore — mettez-le sur le parent.content: attr(data-text) / "" déclare un texte alternatif vide : le pseudo-élément reste affiché mais n'est plus lu (Chrome 77+, Safari 17.4+, Firefox 128+). Les navigateurs qui ignorent la syntaxe ignorent toute la déclaration — gardez la ligne content: attr(data-text) juste avant comme repli.zone-text-reveal, textRevealContainer). Pour plusieurs instances, bouclez sur document.querySelectorAll('.cursor-reveal-container') et écoutez mousemove sur chaque conteneur ou sur un parent commun, en recalculant getBoundingClientRect() du conteneur concerné à chaque événement. Pour la zone d'écoute, c'est déjà prévu : l'écouteur est sur la zone, pas sur le texte, et la règle livrée lui donne width: 100% — toute la surface du parent réagit. Réduisez ou agrandissez cette zone à volonté : le cercle reste calculé par rapport au texte.