Back to Components
Text Scroll & Hover Reveal Effect (GSAP + Clip-Path)
Component

Text Scroll & Hover Reveal Effect (GSAP + Clip-Path)

CodewithLord
December 6, 2025

A smooth scroll-triggered text reveal effect built using GSAP, combined with a hover-activated clip-path animation for an interactive, modern UI. Perfect for portfolio websites, hero sections, and eye-catching text animations.

🔥 Description

A smooth scroll-triggered text reveal effect built using GSAP, combined with a hover-activated clip-path animation for an interactive, modern UI. Perfect for portfolio websites, hero sections, and eye-catching text animations.



🧠 How the Component Works


🔹 HTML Explanation

The HTML contains multiple heading elements wrapped inside a container. Each heading has a span inside it — this span is used for the hover reveal animation. External fonts, stylesheets, and GSAP libraries are included through CDN links, with a clean document structure.

🔹 CSS Explanation

The CSS styles create a full-screen vertical layout with large text. The text is initially transparent but has a background gradient clipped to the text, giving a reveal effect when animated. Each span is positioned on top of the text using absolute positioning and clipped using clip-path, which expands on hover to reveal a colored overlay.

🔹 JavaScript Explanation

The JavaScript uses GSAP’s ScrollTrigger plugin to animate the background clipping as the user scrolls. Each text element increases its background size based on scroll position. The animation is smooth, synchronized with scroll, and triggers individually for each text block.

1<!DOCTYPE html> 2<html lang="en"> 3 4 <head> 5 <meta charset="UTF-8"> 6 <title>three.js + HexaVision Pass</title> 7 8 9 </head> 10 11 <body> 12 <html lang="en"> 13 <head> 14 <meta charset="UTF-8" /> 15 <title>three.js + HexaVision Pass</title> 16 <style> 17 body { 18 margin: 0; 19 overflow: hidden; 20 background: radial-gradient(#000, #111); 21 user-select: none; 22 -webkit-user-select: none; 23 -moz-user-select: none; 24 -ms-user-select: none; 25 } 26 canvas { 27 display: block; 28 } 29 .note { 30 position: absolute; 31 left: 12px; 32 top: 12px; 33 color: #ddd; 34 font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; 35 font-size: 13px; 36 background: rgba(0, 0, 0, 0.5); 37 padding: 8px 10px; 38 border-radius: 6px; 39 backdrop-filter: blur(4px); 40 z-index: 10; 41 } 42 #notification { 43 position: fixed; 44 bottom: -100px; 45 left: 50%; 46 transform: translateX(-50%); 47 background: rgba(0, 0, 0, 0.7); 48 color: #fff; 49 font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; 50 font-size: 13px; 51 padding: 12px 18px; 52 border-radius: 8px; 53 backdrop-filter: blur(6px); 54 display: flex; 55 align-items: center; 56 gap: 12px; 57 z-index: 999; 58 } 59 #notification a { 60 color: orange; 61 text-decoration: none; 62 font-weight: 500; 63 transition: color 0.3s ease; 64 } 65 66 #notification a:hover { 67 color: #ffb347; /* un orange plus clair au survol */ 68 } 69 70 #notification button { 71 background: none; 72 border: none; 73 color: gray; 74 font-size: 16px; 75 cursor: pointer; 76 } 77 78 #notification button:hover { 79 background: none; 80 border: none; 81 color: white; 82 font-size: 16px; 83 cursor: pointer; 84 } 85 86 </style> 87 <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.9/dat.gui.min.js"></script> 88 </head> 89 <body> 90 91 <div id="app"> 92 <div class="note">three.js + HexaVision Pass</div> 93 <div id="notification" class="slide-up"> 94 <span>Curious for more? Check out my other creations <a href="https://codepen.io/vainsan" target="_blank" rel="noopener">here</a> 95 </span> 96 <button id="closeBtn"></button> 97 </div> 98 </div> 99 100 <!-- 101 V2: https://codepen.io/vainsan/pen/LEGKGrY 102 --> 103 104 <script type="module"> 105 import * as THREE from 'https://cdn.skypack.dev/three@0.136.0'; 106 import { EffectComposer } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/postprocessing/EffectComposer.js'; 107 import { RenderPass } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/postprocessing/RenderPass.js'; 108 import { UnrealBloomPass } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/postprocessing/UnrealBloomPass.js'; 109 import { ShaderPass } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/postprocessing/ShaderPass.js'; 110 import { OrbitControls } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/controls/OrbitControls.js'; 111 import { TrackballControls } from 'https://cdn.skypack.dev/three@0.136.0/examples/jsm/controls/TrackballControls.js'; 112 113 function scrambleText(selector, options = {}) { 114 const { 115 speed = 40, 116 duration = 1200, 117 characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}<>?', 118 delayBetween = 20 119 } = options; 120 121 const element = document.querySelector(selector); 122 if (!element) return; 123 124 const originalText = element.textContent; 125 const output = Array(originalText.length).fill(''); 126 const resolveFrames = Array.from({ length: originalText.length }, (_, i) => 127 Math.floor(Math.random() * (duration / speed)) + i * (delayBetween / speed) 128 ); 129 130 // Masquer le texte initial 131 element.textContent = ''; 132 133 let frame = 0; 134 const interval = setInterval(() => { 135 for (let i = 0; i < originalText.length; i++) { 136 if (frame >= resolveFrames[i]) { 137 output[i] = originalText[i]; 138 } else { 139 output[i] = characters.charAt(Math.floor(Math.random() * characters.length)); 140 } 141 } 142 143 element.textContent = output.join(''); 144 frame++; 145 146 if (frame > Math.max(...resolveFrames)) { 147 clearInterval(interval); 148 element.textContent = originalText; 149 } 150 }, speed); 151 } 152 153 154 scrambleText('.note', { 155 speed: 30, 156 duration: 1500, 157 delayBetween: 30 158 }); 159 160 const effectConfig = { 161 enableFlyEffect: true, // État initial 162 toggleFlyEffect: function() { 163 // Basculer l'activation du pass 164 this.enableFlyEffect = !this.enableFlyEffect; 165 if (this.enableFlyEffect) { 166 composer.addPass(flyPass); 167 } else { 168 composer.removePass(flyPass); 169 } 170 flyPass.enabled = this.enableFlyEffect; 171 } 172 }; 173 174 const scene = new THREE.Scene(); 175 const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); 176 camera.position.z = 6; 177 const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); 178 renderer.setSize(window.innerWidth, window.innerHeight); 179 renderer.setPixelRatio(window.devicePixelRatio); 180 document.body.appendChild(renderer.domElement); 181 const controls = new OrbitControls(camera, renderer.domElement); 182 controls.enableDamping = true; 183 controls.dampingFactor = 0.02; 184 controls.maxDistance = 20; 185 controls.minDistance = 0.1; 186 controls.panSpeed = 0.02; 187 controls.rotateSpeed = 0.5; 188 controls.zoomSpeed = 1; 189 controls.enableZoom = false; 190 191 const controls2 = new TrackballControls(camera, renderer.domElement); 192 controls2.noRotate = true; 193 controls2.noPan = true; 194 controls2.noZoom = false; 195 controls2.zoomSpeed = 1.5; 196 197 198 const geometry = new THREE.IcosahedronGeometry(1, 30); 199 // ShaderMaterial for the icosahedron 200 const material = new THREE.ShaderMaterial({ 201 uniforms: { 202 uTime: { value: 0 }, 203 uScale: { value: 6.0 }, 204 uDepth: { value: 1 }, 205 uSharpness: { value: 3.0 }, 206 uSpeed: { value: 0 }, 207 uColor: { value: new THREE.Color('#00ff00') }, 208 uNoiseScale: { value: 1.5 }, 209 uNoiseStrength: { value: 1.4 }, 210 uOutlineWidth: { value: 0.5 }, 211 uOutlineColor: { value: new THREE.Color('#0a0060') }, 212 uSecondaryColor: { value: new THREE.Color('#ff005f') }, 213 uDisplacementStrength: { value: 1 } 214 }, 215 vertexShader: ` 216 uniform float uTime; 217 uniform float uScale; 218 uniform float uSharpness; 219 uniform float uSpeed; 220 uniform float uNoiseScale; 221 uniform float uNoiseStrength; 222 uniform float uDisplacementStrength; 223 varying vec3 vNormal; 224 varying vec3 v3Position; 225 varying float vShellPattern; 226 float hash(vec2 p) { 227 return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); 228 } 229 float noise(vec2 uv, float timeOffset) { 230 vec2 i = floor(uv); 231 vec2 f = fract(uv); 232 float a = hash(i + vec2(timeOffset)); 233 float b = hash(i + vec2(1.0, 0.0) + vec2(timeOffset)); 234 float c = hash(i + vec2(0.0, 1.0) + vec2(timeOffset)); 235 float d = hash(i + vec2(1.0, 1.0) + vec2(timeOffset)); 236 vec2 u = f * f * (3.0 - 2.0 * f); 237 return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); 238 } 239 float voronoi(vec2 uv, float t) { 240 vec2 g = floor(uv); 241 vec2 f = fract(uv); 242 float minDist1 = 1.0; 243 float secondMinDist1 = 1.0; 244 float minDist2 = 1.0; 245 float secondMinDist2 = 1.0; 246 float t0 = t; 247 float t1 = t + 1.0; 248 float a = smoothstep(0.0, 1.0, fract(t)); 249 for (int y = -1; y <= 1; y++) { 250 for (int x = -1; x <= 1; x++) { 251 vec2 lattice = vec2(x, y); 252 vec2 perturbed_lattice = lattice + uNoiseStrength * (noise((g + lattice) * uNoiseScale, t0) * 2.0 - 1.0); 253 vec2 point = hash(g + perturbed_lattice) + perturbed_lattice - f; 254 float dist = length(point); 255 if (dist < minDist1) { 256 secondMinDist1 = minDist1; 257 minDist1 = dist; 258 } else if (dist < secondMinDist1) { 259 secondMinDist1 = dist; 260 } 261 } 262 } 263 for (int y = -1; y <= 1; y++) { 264 for (int x = -1; x <= 1; x++) { 265 vec2 lattice = vec2(x, y); 266 vec2 perturbed_lattice = lattice + uNoiseStrength * (noise((g + lattice) * uNoiseScale, t1) * 2.0 - 1.0); 267 vec2 point = hash(g + perturbed_lattice) + perturbed_lattice - f; 268 float dist = length(point); 269 if (dist < minDist2) { 270 secondMinDist2 = minDist2; 271 minDist2 = dist; 272 } else if (dist < secondMinDist2) { 273 secondMinDist2 = dist; 274 } 275 } 276 } 277 float pattern1 = secondMinDist1 - minDist1; 278 float pattern2 = secondMinDist2 - minDist2; 279 return mix(pattern1, pattern2, a); 280 } 281 float triplanar(vec3 p, vec3 normal, float t) { 282 vec3 blending = abs(normal); 283 blending = normalize(max(blending, 0.00001)); 284 blending /= (blending.x + blending.y + blending.z); 285 float x = voronoi(p.yz * uScale, t); 286 float y = voronoi(p.xz * uScale, t); 287 float z = voronoi(p.xy * uScale, t); 288 return (x * blending.x + y * blending.y + z * blending.z); 289 } 290 void main() { 291 vec3 transformedNormal = normalize(normalMatrix * normal); 292 vec3 displacedPosition = position; 293 float time = uTime * uSpeed; 294 float patternValue = triplanar(position, normal, time); 295 vShellPattern = patternValue; 296 float softPattern = smoothstep(0.2, 0.8, patternValue); 297 float displacementFactor = softPattern * uDisplacementStrength; 298 displacedPosition += normal * displacementFactor; 299 vNormal = transformedNormal; 300 v3Position = position; 301 gl_Position = projectionMatrix * modelViewMatrix * vec4(displacedPosition, 1.0); 302 } 303 `, 304 fragmentShader: ` 305 uniform float uTime; 306 uniform float uDepth; 307 uniform vec3 uColor; 308 uniform float uOutlineWidth; 309 uniform vec3 uOutlineColor; 310 uniform vec3 uSecondaryColor; 311 varying vec3 vNormal; 312 varying vec3 v3Position; 313 varying float vShellPattern; 314 void main() { 315 float steppedPattern = smoothstep(uOutlineWidth, uOutlineWidth + 0.2, vShellPattern); 316 vec3 lightDirection = normalize(vec3(0.5, 0.5, 1.0)); 317 float lighting = dot(vNormal, lightDirection) * 0.5 + 0.5; 318 vec3 baseColor = mix(uOutlineColor, uSecondaryColor, steppedPattern); 319 float highlightIntensity = smoothstep(0.0, 0.5, vShellPattern); 320 vec3 finalColor = baseColor + uColor * highlightIntensity * uDepth * lighting; 321 gl_FragColor = vec4(finalColor * lighting, 1.0); 322 } 323 `, 324 wireframe: true, 325 }); 326 const mesh = new THREE.Mesh(geometry, material); 327 scene.add(mesh); 328 const light = new THREE.DirectionalLight(0xffffff, 1); 329 light.position.set(5, 5, 5); 330 scene.add(light); 331 const composer = new EffectComposer(renderer); 332 composer.addPass(new RenderPass(scene, camera)); 333 const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.3, 0.4, 0.0); 334 335 const flyShader = { 336 uniforms: { 337 tDiffuse: { value: null }, 338 resolution: { value: new THREE.Vector2(window.innerWidth * window.devicePixelRatio, window.innerHeight * window.devicePixelRatio) }, 339 time: { value: 0 }, 340 ommatidiaSize: { value: 4.0 }, 341 }, 342 vertexShader: ` 343 varying vec2 vUv; 344 void main() { 345 vUv = uv; 346 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 347 } 348 `, 349 fragmentShader: ` 350 precision highp float; 351 352 varying vec2 vUv; 353 uniform sampler2D tDiffuse; 354 uniform vec2 resolution; 355 uniform float ommatidiaSize; 356 357 // Grille hexagonale orientée verticalement (pointes en haut/bas) 358 vec2 hexCoord(vec2 uv, float size) { 359 vec2 r = resolution / size; 360 uv *= r; 361 362 // Décalage horizontal sur une ligne sur deux 363 float row = floor(uv.y); 364 float col = floor(uv.x - mod(row, 2.0) * 0.5); 365 366 vec2 hexUV = vec2(col + 0.5 * mod(row, 2.0), row); 367 hexUV /= r; 368 369 return hexUV; 370 } 371 372 373 // Masque hexagonal orienté verticalement 374 float hexMask(vec2 uv, float size) { 375 vec2 p = uv * resolution / size; 376 p = fract(p) - 0.5; 377 378 // Transformation pour hexagone vertical 379 p.y *= 1.0; 380 p.x *= 0.57735; // sqrt(3)/3 381 382 p = abs(p); 383 float a = max(p.y * 0.866025 + p.x, p.x * 2.0); // 0.866 = cos(30°) 384 return step(a, 0.5); 385 } 386 387 void main() { 388 vec2 hexUV = hexCoord(vUv, ommatidiaSize); 389 vec4 color = texture2D(tDiffuse, hexUV); 390 391 float mask = hexMask(vUv, ommatidiaSize); 392 gl_FragColor = color * mask; 393 } 394 395 ` 396 }; 397 const flyPass = new ShaderPass(flyShader); 398 flyPass.renderToScreen = true; 399 composer.addPass(flyPass); 400 composer.addPass(bloomPass); 401 const gui = new dat.GUI(); 402 gui.close(); 403 404 function getRandomColor() { 405 const letters = '~#{[|`\^@]@^---\`[|{[##@~@^{{~#}]}|--<>]]}'; 406 let color = '#'; 407 for (let i = 0; i < 6; i++) { 408 color += letters[Math.floor(Math.random() * 16)]; 409 } 410 return color; 411 } 412 413 const randomizer = { 414 randomize: () => { 415 material.uniforms.uScale.value = 1 + Math.random() * 19; 416 material.uniforms.uDepth.value = Math.random() * 2; 417 material.uniforms.uSharpness.value = 1 + Math.random() * 9; 418 material.uniforms.uColor.value.set(getRandomColor()); 419 material.uniforms.uNoiseScale.value = 0.1 + Math.random() * 4.9; 420 material.uniforms.uNoiseStrength.value = Math.random() * 1; 421 material.uniforms.uOutlineWidth.value = 0.01 + Math.random() * 0.99; 422 material.uniforms.uOutlineColor.value.set(getRandomColor()); 423 material.uniforms.uSecondaryColor.value.set(getRandomColor()); 424 material.uniforms.uDisplacementStrength.value = -0.5 + Math.random() * 5.5; 425 for (let i in gui.__folders) { 426 for (let j in gui.__folders[i].__controllers) { 427 gui.__folders[i].__controllers[j].updateDisplay(); 428 } 429 } 430 } 431 }; 432 renderer.domElement.addEventListener('dblclick', () => { 433 randomizer.randomize(); 434 }); 435 const flyFolder = gui.addFolder('ShaderPass'); 436 flyFolder.add(flyPass, 'enabled').name('HexaVision'); 437 flyFolder.add(flyPass.material.uniforms.ommatidiaSize, 'value', 2.0, 200).step(1).name('Ommatidia Size'); 438 flyFolder.add(randomizer, 'randomize').name('Randomize All (Double Click)'); 439 const guiControls = { wireframe: true }; 440 flyFolder.add(guiControls, 'wireframe').name('Toggle Wireframe').onChange(value => { 441 material.wireframe = value; 442 }); 443 flyFolder.open(); 444 445 446 const patternFolder = gui.addFolder('Shader Pattern'); 447 patternFolder.add(material.uniforms.uScale, 'value', 1, 20).name('Pattern Scale'); 448 patternFolder.add(material.uniforms.uDepth, 'value', 0, 2).name('Highlight Depth'); 449 patternFolder.add(material.uniforms.uSharpness, 'value', 1, 10).name('Pattern Sharpness'); 450 // patternFolder.add(material.uniforms.uSpeed, 'value', 0.00000, 1.00000).step(0.00001).name('Pattern Speed'); 451 patternFolder.addColor({ color: '#ff00f1' }, 'color').onChange(val => { 452 material.uniforms.uColor.value.set(val); 453 }).name('Highlight Color'); 454 // patternFolder.open(); 455 456 const displacementFolder = gui.addFolder('Displacement'); 457 displacementFolder.add(material.uniforms.uNoiseScale, 'value', 0.1, 5.0).name('Noise Scale'); 458 displacementFolder.add(material.uniforms.uNoiseStrength, 'value', 0.0, 1.0).name('Noise Strength'); 459 displacementFolder.add(material.uniforms.uOutlineWidth, 'value', 0.01, 1).name('Outline Width'); 460 displacementFolder.addColor({ outline: '#0a0060' }, 'outline').onChange(val => { 461 material.uniforms.uOutlineColor.value.set(val); 462 }).name('Outline Color'); 463 displacementFolder.addColor({ secondary: '#ff005f' }, 'secondary').onChange(val => { 464 material.uniforms.uSecondaryColor.value.set(val); 465 }).name('Scale Fill Color'); 466 displacementFolder.add(material.uniforms.uDisplacementStrength, 'value', -0.5, 5).name('Displacement Strength'); 467 // displacementFolder.open(); 468 const bloomFolder = gui.addFolder('Bloom'); 469 bloomFolder.add(bloomPass, 'strength', 0.0, 3.0).name('Strength'); 470 bloomFolder.add(bloomPass, 'radius', 0.0, 1.0).name('Radius'); 471 bloomFolder.add(bloomPass, 'threshold', 0.0, 1.0).name('Threshold'); 472 // bloomFolder.open(); 473 474 window.addEventListener('resize', () => { 475 camera.aspect = window.innerWidth / window.innerHeight; 476 camera.updateProjectionMatrix(); 477 renderer.setSize(window.innerWidth, window.innerHeight); 478 composer.setSize(window.innerWidth, window.innerHeight); 479 flyPass.material.uniforms.resolution.value.set(window.innerWidth * window.devicePixelRatio, window.innerHeight * window.devicePixelRatio); 480 }); 481 function animate(time) { 482 material.uniforms.uTime.value = time * 0.001; 483 flyPass.material.uniforms.time.value = time * 0.001; 484 mesh.rotation.set( 485 mesh.rotation.x + 0.0002, 486 mesh.rotation.y + 0.001, 487 mesh.rotation.z + 0.0002 488 ); 489 490 const target = controls.target; 491 controls2.target.set(target.x, target.y, target.z); 492 controls.update(); 493 controls2.update(); 494 495 composer.render(); 496 requestAnimationFrame(animate); 497 } 498 animate(); 499 500 import anime from 'https://cdn.skypack.dev/animejs@3.2.1'; 501 502 const notif = document.getElementById('notification'); 503 const closeBtn = document.getElementById('closeBtn'); 504 505 // Slide in from bottom 506 anime({ 507 targets: notif, 508 bottom: '24px', 509 easing: 'easeOutExpo', 510 duration: 800, 511 delay: 1000 512 }); 513 514 // Slide out to bottom on close 515 closeBtn.addEventListener('click', () => { 516 anime({ 517 targets: notif, 518 bottom: '-100px', 519 easing: 'easeInExpo', 520 duration: 600 521 }); 522 }); 523 524 </script> 525 </body> 526</html> 527 528 </body> 529 530</html> 531

Love this component?

Explore more components and build amazing UIs.

View All Components