Live preview needs JavaScript. The code below works without any dependencies.
Prompt for your AI assistant
Paste this into Claude, Cursor or ChatGPT. The settings update as you move the controls above.
Build "Gradient hands hero" for my site using the Enrichr Library element at https://enrichr.dev/elements/gradient-hands-hero.html.
Read the machine-readable spec and source at https://enrichr.dev/elements/gradient-hands-hero.json (markup, CSS, JavaScript, parameters).
Requirements: vanilla JavaScript and CSS, no new dependencies; keep the behaviour of the source unchanged; respect prefers-reduced-motion; work on touch screens; keep it accessible.
Settings:
- headline: Pages|that wave|back
- badge: Now in|the Library|+
- prompt: Scroll and see what it can do
- from: #005236
- to: #e6ff55
- handLeft: #0a6b46
- handRight: #005236
- accent: #e6ff55
- ink: #0c1e18
- paper: #fbf8f1
Verify: reduced motion shows no movement, it works at 375px wide, there are no console errors, and scrolling stays smooth.The same spec as JSON, for tools that read it directly: /elements/gradient-hands-hero.json.
Code
Mount it on an element that contains the markup. It returns an object with update(options) and destroy().
HTML
<div class="ef-ghh">
<div class="ef-ghh__logo"><svg viewBox="0 0 50 50" aria-hidden="true" focusable="false"><path fill="currentColor" d="m34.213 25.361 c0 2.655 1.381 4.808 3.086 4.808 0.781 0 1.494 -0.452 2.038 -1.198 0.267 -0.366 0.688 -0.589 1.142 -0.589 h0.054 c0.774 0 1.401 0.627 1.401 1.401 v10.65 h-10.651 c-0.774 0 -1.401 0.627 -1.401 1.401 v0.054 c0 0.453 0.222 0.874 0.589 1.141 0.746 0.544 1.198 1.257 1.198 2.038 0 1.705 -2.152 3.086 -4.808 3.086 -2.655 0 -4.808 -1.381 -4.808 -3.086 0 -0.781 0.452 -1.494 1.198 -2.038 0.366 -0.267 0.589 -0.688 0.589 -1.142 v-0.054 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-10.65 v-10.651 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-0.054 c-0.453 0 -0.875 0.222 -1.142 0.589 -0.543 0.746 -1.257 1.198 -2.038 1.198 -1.705 0 -3.086 -2.152 -3.086 -4.808 0 -2.655 1.381 -4.808 3.086 -4.808 0.781 0 1.495 0.452 2.038 1.198 0.267 0.366 0.688 0.589 1.141 0.589 h0.054 c0.774 0 1.401 -0.627 1.401 -1.401 v-10.65 h10.65 c0.774 0 1.401 -0.627 1.401 -1.401 v-0.054 c0 -0.453 -0.222 -0.875 -0.589 -1.142 -0.746 -0.543 -1.198 -1.257 -1.198 -2.038 0 -1.705 2.152 -3.086 4.808 -3.086 2.655 0 4.808 1.381 4.808 3.086 0 0.781 -0.452 1.495 -1.198 2.038 -0.366 0.267 -0.589 0.688 -0.589 1.141 v0.054 c0 0.774 0.627 1.401 1.401 1.401 h10.65 v10.65 c0 0.774 -0.627 1.401 -1.401 1.401 h-0.053 c-0.453 0 -0.874 -0.222 -1.141 -0.589 -0.544 -0.746 -1.257 -1.198 -2.038 -1.198 -1.705 0 -3.086 2.152 -3.086 4.808 z"/></svg><span class="ef-ghh__word">Enrichr</span></div>
<div class="ef-ghh__badge ef-ghh__hot"><span class="ef-ghh__badge-text">Now in<br>the Library<br>+</span></div>
<div class="ef-ghh__hero">
<h2 class="ef-ghh__title"></h2>
<div class="ef-ghh__scroll ef-ghh__hot">Scroll and see what it can do <i class="ef-ghh__bob" aria-hidden="true">↓</i></div>
<svg class="ef-ghh__hand ef-ghh__hand--l" viewBox="0 0 300 340" aria-hidden="true" focusable="false">
<defs><linearGradient class="ef-ghh__gl" x1="0" y1="1" x2="1" y2="0"><stop offset="0" class="ef-ghh__gl0"/><stop offset="1" class="ef-ghh__gl1"/></linearGradient></defs>
<g class="ef-ghh__hand-g" stroke-width="5" stroke-linejoin="round" stroke-linecap="round">
<path class="ef-ghh__palm" d="M72 196 L228 196 C248 226 254 266 242 304 L230 342 L90 342 C82 304 68 256 72 196 Z"/>
<path class="ef-ghh__palm" d="M62 200 C53 172 50 148 54 130 C58 112 79 110 85 125 C91 146 95 172 100 202 Z"/>
<path class="ef-ghh__palm" d="M99 200 C91 156 87 114 93 88 C98 66 124 64 131 85 C139 113 139 154 142 200 Z"/>
<path class="ef-ghh__palm" d="M141 202 C135 146 135 94 141 70 C147 46 175 46 181 70 C187 94 185 146 186 202 Z"/>
<path class="ef-ghh__palm" d="M185 200 C185 156 187 112 195 90 C203 66 231 70 233 94 C235 124 231 162 227 204 Z"/>
<path class="ef-ghh__palm" d="M212 262 C236 244 262 214 274 182 C283 160 270 142 253 148 C238 154 230 178 217 202 Z"/>
<g fill="none" stroke="var(--ef-ghh-ink)" stroke-opacity="0.32" stroke-width="3">
<path d="M64 168 Q74 172 86 166 M70 142 Q76 146 84 142"/>
<path d="M97 150 Q112 156 130 148 M98 118 Q112 124 128 116 M103 92 Q112 96 124 92"/>
<path d="M142 140 Q161 147 184 138 M142 108 Q161 115 183 106 M146 78 Q162 84 178 76"/>
<path d="M187 150 Q208 156 228 148 M190 120 Q210 126 230 118"/>
<path d="M102 244 Q150 268 214 236"/>
<path d="M98 270 Q146 296 206 276"/>
<path d="M214 214 Q196 256 206 306"/>
<path d="M240 168 Q252 172 262 166"/>
</g>
<g fill="var(--ef-ghh-paper)" fill-opacity="0.42" stroke="none">
<path d="M60 126 Q69 114 80 124 Q70 130 60 126 Z"/>
<path d="M100 84 Q112 70 124 84 Q112 90 100 84 Z"/>
<path d="M148 66 Q161 50 175 66 Q161 74 148 66 Z"/>
<path d="M197 88 Q210 72 224 88 Q210 96 197 88 Z"/>
<path d="M258 152 Q267 146 270 160 Q262 164 258 152 Z"/>
</g>
<g fill="var(--ef-ghh-paper)" fill-opacity="0.16" stroke="none">
<path d="M76 204 L100 204 C98 250 104 300 100 340 L92 340 C84 300 72 252 76 204 Z"/>
<path d="M146 80 C144 120 144 160 146 196 L154 196 C152 160 154 120 156 84 Z"/>
</g>
</g>
</svg>
<svg class="ef-ghh__hand ef-ghh__hand--r" viewBox="0 0 300 340" aria-hidden="true" focusable="false">
<defs><linearGradient class="ef-ghh__gr" x1="1" y1="1" x2="0" y2="0"><stop offset="0" class="ef-ghh__gr0"/><stop offset="1" class="ef-ghh__gr1"/></linearGradient></defs>
<g class="ef-ghh__hand-g" stroke-width="5" stroke-linejoin="round" stroke-linecap="round" transform="translate(300,0) scale(-1,1)">
<path class="ef-ghh__palm" d="M72 196 L228 196 C248 226 254 266 242 304 L230 342 L90 342 C82 304 68 256 72 196 Z"/>
<path class="ef-ghh__palm" d="M62 200 C53 172 50 148 54 130 C58 112 79 110 85 125 C91 146 95 172 100 202 Z"/>
<path class="ef-ghh__palm" d="M99 200 C91 156 87 114 93 88 C98 66 124 64 131 85 C139 113 139 154 142 200 Z"/>
<path class="ef-ghh__palm" d="M141 202 C135 146 135 94 141 70 C147 46 175 46 181 70 C187 94 185 146 186 202 Z"/>
<path class="ef-ghh__palm" d="M185 200 C185 156 187 112 195 90 C203 66 231 70 233 94 C235 124 231 162 227 204 Z"/>
<path class="ef-ghh__palm" d="M212 262 C236 244 262 214 274 182 C283 160 270 142 253 148 C238 154 230 178 217 202 Z"/>
<g fill="none" stroke="var(--ef-ghh-ink)" stroke-opacity="0.32" stroke-width="3">
<path d="M64 168 Q74 172 86 166 M70 142 Q76 146 84 142"/>
<path d="M97 150 Q112 156 130 148 M98 118 Q112 124 128 116 M103 92 Q112 96 124 92"/>
<path d="M142 140 Q161 147 184 138 M142 108 Q161 115 183 106 M146 78 Q162 84 178 76"/>
<path d="M187 150 Q208 156 228 148 M190 120 Q210 126 230 118"/>
<path d="M102 244 Q150 268 214 236"/>
<path d="M98 270 Q146 296 206 276"/>
<path d="M214 214 Q196 256 206 306"/>
<path d="M240 168 Q252 172 262 166"/>
</g>
<g fill="var(--ef-ghh-paper)" fill-opacity="0.42" stroke="none">
<path d="M60 126 Q69 114 80 124 Q70 130 60 126 Z"/>
<path d="M100 84 Q112 70 124 84 Q112 90 100 84 Z"/>
<path d="M148 66 Q161 50 175 66 Q161 74 148 66 Z"/>
<path d="M197 88 Q210 72 224 88 Q210 96 197 88 Z"/>
<path d="M258 152 Q267 146 270 160 Q262 164 258 152 Z"/>
</g>
<g fill="var(--ef-ghh-paper)" fill-opacity="0.16" stroke="none">
<path d="M76 204 L100 204 C98 250 104 300 100 340 L92 340 C84 300 72 252 76 204 Z"/>
<path d="M146 80 C144 120 144 160 146 196 L154 196 C152 160 154 120 156 84 Z"/>
</g>
</g>
</svg>
</div>
<a class="ef-ghh__faq ef-ghh__hot" href="#">FAQ <b aria-hidden="true">↑</b></a>
<div class="ef-ghh__cursor" aria-hidden="true"></div>
</div>CSS
/* Enrichr Library · https://enrichr.dev */
.ef-ghh { --ef-ghh-a: #005236; --ef-ghh-b: #e6ff55; --ef-ghh-ink: #0c1e18; --ef-ghh-acc: #e6ff55; --ef-ghh-paper: #fbf8f1;
position: relative; flex: 1 0 auto; width: 100%; height: 100%; min-height: 36rem; box-sizing: border-box; overflow: hidden; border-radius: 1rem; isolation: isolate;
background: linear-gradient(135deg, var(--ef-ghh-a) 0%, color-mix(in srgb, var(--ef-ghh-a) 72%, var(--ef-ghh-b)) 26%, color-mix(in srgb, var(--ef-ghh-a) 46%, var(--ef-ghh-b)) 52%, color-mix(in srgb, var(--ef-ghh-a) 18%, var(--ef-ghh-b)) 78%, var(--ef-ghh-b) 100%);
color: var(--ef-ghh-ink); font-family: "Parkinsans", "Space Grotesk", sans-serif; container-type: inline-size; touch-action: pan-y; }
.ef-ghh *, .ef-ghh *::before, .ef-ghh *::after { box-sizing: border-box; }
@media (pointer: fine) { .ef-ghh, .ef-ghh a { cursor: none; } }
.ef-ghh__logo { position: absolute; top: 26px; left: 50%; z-index: 8; transform: translateX(-50%); display: flex; align-items: center; gap: 8px; padding: 8px 18px 8px 14px; border: 2px solid var(--ef-ghh-ink); border-radius: 999px; background: var(--ef-ghh-acc); box-shadow: 0 3px 0 var(--ef-ghh-ink); font-weight: 700; font-size: 17px; line-height: 1; white-space: nowrap; }
.ef-ghh__logo svg { width: 22px; height: 22px; display: block; }
.ef-ghh__word { letter-spacing: 0.01em; }
.ef-ghh__badge { position: absolute; top: 34px; right: 4cqi; z-index: 8; width: 96px; height: 96px; border-radius: 50%; display: flex; align-items: center; justify-content: center; text-align: center; background: var(--ef-ghh-paper); border: 2px solid var(--ef-ghh-ink); font-weight: 800; font-size: 13px; line-height: 1.15; text-transform: uppercase; transform: rotate(12deg); animation: ef-ghh-wob 5s ease-in-out infinite; }
.ef-ghh__badge-text { display: block; }
@keyframes ef-ghh-wob { 0%, 100% { transform: rotate(9deg); } 50% { transform: rotate(17deg) scale(1.04); } }
.ef-ghh__hero { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 5.5rem 6cqi; }
.ef-ghh .ef-ghh__title { position: relative; z-index: 5; margin: 0; max-width: none; font-family: "Parkinsans", "Space Grotesk", sans-serif; font-size: clamp(3.6rem, 11.5cqi, 11.25rem); font-weight: 800; letter-spacing: 0.005em; line-height: 0.96; text-align: center; text-transform: uppercase; color: var(--ef-ghh-ink); opacity: 1; will-change: transform, opacity; }
.ef-ghh__scroll { position: relative; z-index: 6; margin-top: 2.2rem; display: inline-flex; align-items: center; gap: 12px; padding: 15px 30px; border: 2px solid var(--ef-ghh-ink); border-radius: 999px; background: var(--ef-ghh-paper); box-shadow: 0 4px 0 rgba(0, 0, 0, 0.85); font-family: "Instrument Sans", system-ui, sans-serif; font-size: 15px; font-weight: 600; line-height: 1.2; }
.ef-ghh__bob { width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; background: var(--ef-ghh-acc); border: 2px solid var(--ef-ghh-ink); font-style: normal; animation: ef-ghh-bob 1.6s infinite; }
@keyframes ef-ghh-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(4px); } }
.ef-ghh__hand { position: absolute; bottom: -6%; z-index: 2; width: clamp(260px, 34cqi, 520px); height: auto; overflow: visible; transform-origin: 50% 100%; will-change: transform; filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.15)); }
.ef-ghh__hand--l { left: -4cqi; transform: rotate(-8deg); }
.ef-ghh__hand--r { right: -4cqi; transform: rotate(8deg); }
.ef-ghh__hand-g { stroke: var(--ef-ghh-ink); }
.ef-ghh__faq { position: absolute; left: 24px; bottom: 22px; z-index: 8; display: flex; align-items: center; gap: 8px; padding: 12px 20px; border: 2px solid var(--ef-ghh-ink); border-radius: 999px; background: var(--ef-ghh-acc); box-shadow: 0 4px 0 rgba(0, 0, 0, 0.85); color: var(--ef-ghh-ink); font-family: "Instrument Sans", system-ui, sans-serif; font-weight: 700; font-size: 14px; line-height: 1; text-decoration: none; }
.ef-ghh__faq b { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border: 2px solid var(--ef-ghh-ink); border-radius: 50%; font-size: 12px; }
.ef-ghh__faq:focus-visible { outline: 3px solid var(--ef-ghh-ink); outline-offset: 3px; }
.ef-ghh__cursor { position: absolute; left: 0; top: 0; z-index: 20; width: 18px; height: 18px; margin: -9px 0 0 -9px; border-radius: 50%; background: #fff; mix-blend-mode: difference; pointer-events: none; opacity: 0; transition: width 0.2s, height 0.2s, margin 0.2s, opacity 0.2s; }
.ef-ghh__cursor.is-on { opacity: 1; }
.ef-ghh__cursor.is-big { width: 52px; height: 52px; margin: -26px 0 0 -26px; }
@media (pointer: coarse) { .ef-ghh__cursor { display: none; } }
@container (max-width: 820px) {
.ef-ghh__badge { width: 76px; height: 76px; font-size: 11px; }
}
.ef-ghh.is-off .ef-ghh__badge, .ef-ghh.is-off .ef-ghh__bob { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
.ef-ghh__badge, .ef-ghh__bob { animation: none; }
.ef-ghh__cursor { transition: none; }
}JavaScript
// Enrichr Library · https://enrichr.dev
// Gradient hands hero: a loud diagonal gradient, a huge three-line headline, a wobbling round badge, a logo pill and a
// scroll prompt with a bobbing arrow, framed by two drawn hands (palm, four fingers, thumb, knuckle creases, nails and palm lines) at the bottom corners. Each frame the hands sway
// on their own slow sine and cosine waves (3 degrees, at 1.2 and 1.1 times an idle clock), lean up to 5 degrees toward
// the side the pointer is on, and when the hero is scrolled past they sink by up to 60% of its height while the headline
// lifts by 18% and fades. The scroll value is eased 8.5% of the way each frame, like the smooth scroll of the original.
// A white dot with a difference blend follows the pointer and swells over the pills and the badge.
// Reduced motion holds the hands at rest and stops the wobble, bob and scroll reactions. The loop pauses off screen and in
// hidden tabs.
// mount(root, { headline (lines separated by |), badge (lines separated by |), prompt, from (gradient start), to (gradient end),
// handLeft, handRight, accent (pills), ink, paper (badge and prompt) })
function mount(root, o) {
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var box = root.querySelector(".ef-ghh");
var title = root.querySelector(".ef-ghh__title");
var badge = root.querySelector(".ef-ghh__badge-text");
var scrollBtn = root.querySelector(".ef-ghh__scroll");
var handL = root.querySelector(".ef-ghh__hand--l");
var handR = root.querySelector(".ef-ghh__hand--r");
var cursor = root.querySelector(".ef-ghh__cursor");
var uid = "ef-ghh-" + Math.random().toString(36).slice(2, 8);
var opts = {}, raf = 0, last = 0, idle = 0, cur = 0, visible = true, destroyed = false, io = null, ro = null, offs = [];
var W = 1, H = 1, mouse = { x: 0.5, y: 0.5 };
[["ef-ghh__gl", handL], ["ef-ghh__gr", handR]].forEach(function (g, i) {
var lg = g[1].querySelector("." + g[0]); lg.id = uid + "-" + i;
Array.prototype.forEach.call(g[1].querySelectorAll(".ef-ghh__palm"), function (p) { p.setAttribute("fill", "url(#" + lg.id + ")"); });
});
function hex(h, d) { var m = /^#?([0-9a-f]{6})$/i.exec(h || ""); var v = parseInt(m ? m[1] : d, 16); return [v >> 16, (v >> 8) & 255, v & 255]; }
function mix(a, b, t) { var x = hex(a, "000000"), y = hex(b, "ffffff"); return "rgb(" + x.map(function (v, i) { return Math.round(v + (y[i] - v) * t); }).join(",") + ")"; }
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """ }[c]; }); }
function frame(dt) {
var r = box.getBoundingClientRect();
var target = Math.max(0, -r.top);
if (reduce) cur = 0;
else { cur += (target - cur) * (1 - Math.pow(1 - 0.085, dt * 60)); if (Math.abs(cur - target) < 0.05) cur = target; }
idle += reduce ? 0 : 0.016 * dt * 60;
var mR = reduce ? 0 : mouse.x - 0.5;
var gone = Math.min(cur / (H * 0.9), 1);
var sway = reduce ? 0 : 1;
handL.style.transform = "translateY(" + (gone * 0.6 * H).toFixed(1) + "px) rotate(" + (-8 + Math.sin(idle * 1.2) * 3 * sway + mR * 10).toFixed(3) + "deg)";
handR.style.transform = "translateY(" + (gone * 0.6 * H).toFixed(1) + "px) rotate(" + (8 + Math.cos(idle * 1.1) * 3 * sway + mR * 10).toFixed(3) + "deg)";
title.style.transform = gone ? "translateY(" + (-gone * 0.18 * H).toFixed(1) + "px)" : "";
title.style.opacity = (1 - gone).toFixed(3);
}
function loop(now) {
raf = requestAnimationFrame(loop);
var dt = Math.min(0.05, (now - last) / 1000 || 0); last = now;
if (!visible || document.hidden) return;
frame(dt);
}
function size() { W = Math.max(1, box.clientWidth); H = Math.max(1, box.clientHeight); if (reduce) frame(0); }
function on(t, type, f, opt) { t.addEventListener(type, f, opt); offs.push(function () { t.removeEventListener(type, f, opt); }); }
on(box, "pointermove", function (e) {
var r = box.getBoundingClientRect(), x = e.clientX - r.left, y = e.clientY - r.top;
mouse.x = x / Math.max(1, r.width); mouse.y = y / Math.max(1, r.height);
cursor.style.transform = "translate(" + x.toFixed(1) + "px," + y.toFixed(1) + "px)";
if (e.pointerType === "mouse") cursor.classList.add("is-on");
}, { passive: true });
on(box, "pointerleave", function () { cursor.classList.remove("is-on", "is-big"); });
on(box, "pointerover", function (e) { if (e.target.closest && e.target.closest(".ef-ghh__hot")) cursor.classList.add("is-big"); });
on(box, "pointerout", function (e) { var t = e.target.closest && e.target.closest(".ef-ghh__hot"); if (t && !(e.relatedTarget && t.contains(e.relatedTarget))) cursor.classList.remove("is-big"); });
function update(next) {
opts = Object.assign({ headline: "Pages|that wave|back", badge: "Now in|the Library|+", prompt: "Scroll and see what it can do", from: "#005236", to: "#e6ff55", handLeft: "#0a6b46", handRight: "#005236", accent: "#e6ff55", ink: "#0c1e18", paper: "#fbf8f1" }, opts, next || {});
var lines = String(opts.headline).split("|").map(function (s) { return s.trim(); }).filter(Boolean);
title.innerHTML = lines.map(esc).join("<br>");
badge.innerHTML = String(opts.badge).split("|").map(function (s) { return esc(s.trim()); }).join("<br>");
scrollBtn.firstChild.nodeValue = opts.prompt + " ";
box.style.setProperty("--ef-ghh-a", opts.from); box.style.setProperty("--ef-ghh-b", opts.to);
box.style.setProperty("--ef-ghh-acc", opts.accent); box.style.setProperty("--ef-ghh-ink", opts.ink); box.style.setProperty("--ef-ghh-paper", opts.paper);
var sl = handL.querySelectorAll("stop"), sr = handR.querySelectorAll("stop");
sl[0].setAttribute("stop-color", opts.handLeft); sl[1].setAttribute("stop-color", mix(opts.handLeft, opts.paper, 0.42));
sr[0].setAttribute("stop-color", opts.handRight); sr[1].setAttribute("stop-color", mix(opts.handRight, opts.to, 0.45));
if (reduce) frame(0);
}
update(o);
size();
frame(0);
if (!reduce) {
if ("IntersectionObserver" in window) { io = new IntersectionObserver(function (e) { visible = e[0].isIntersecting; box.classList.toggle("is-off", !visible); }); io.observe(box); }
last = performance.now(); raf = requestAnimationFrame(loop);
}
if ("ResizeObserver" in window) { ro = new ResizeObserver(function () { if (!destroyed) size(); }); ro.observe(box); }
return {
update: update,
destroy: function () {
destroyed = true; cancelAnimationFrame(raf);
offs.forEach(function (f) { f(); }); offs = [];
if (io) io.disconnect(); if (ro) ro.disconnect();
}
};
}
// Usage
var fx = mount(document.querySelector("#my-root"), {"headline":"Pages|that wave|back","badge":"Now in|the Library|+","prompt":"Scroll and see what it can do","from":"#005236","to":"#e6ff55","handLeft":"#0a6b46","handRight":"#005236","accent":"#e6ff55","ink":"#0c1e18","paper":"#fbf8f1"});How it works
The background is one 135 degree linear gradient with five stops mixed from the two gradient colours, so it stays loud and smooth for any pair. The hands are one SVG path each, filled with their own gradient and outlined in ink, pinned to the bottom corners with their pivot at the wrist. One requestAnimationFrame loop drives them: an idle clock adds 0.016 per frame at 60 fps, the left hand turns -8 degrees plus a sine wave of 3 degrees at 1.2 times the clock, the right hand 8 degrees plus a cosine wave at 1.1 times, and both add up to 5 degrees toward the side of the element the pointer is on. The distance the hero has scrolled above the top of the screen is eased 8.5% of the way per frame, like the smooth scroll of the original page; as it reaches 90% of the hero's height the hands sink by 60% of the height and the headline rises by 18% and fades out. The badge wobbles between 9 and 17 degrees over 5 seconds and the arrow in the scroll prompt bobs 4 px every 1.6 seconds, both in CSS. A white dot with a difference blend follows the pointer and swells to 52 px over the pills and the badge.
Accessibility and performance
The headline, badge, prompt and FAQ link are real text; the hands and the pointer dot are hidden from screen readers. With prefers-reduced-motion the hands rest at their tilt, the badge and arrow stop, and scrolling no longer moves anything. The FAQ link keeps a visible focus ring, and the system cursor comes back on touch screens.
Each frame sets three transforms and one opacity; there is no layout work. The hands are SVG with a drop shadow, promoted to their own layers so the shadow is painted once. The loop pauses off screen and in hidden tabs, and the CSS wobble and bob pause off screen too.
| Section | Hero |
|---|---|
| Techniques | Pointer interaction, Scroll-driven |
| Stack | Vanilla JS, SVG |
| Works with | React, Next.js, Vanilla |
| Dependencies | None |
| Status | tested |
| On CodePen | Open the live pen on our CodePen account; fork it to experiment. |
| Inspired by | Playful gradient hands hero study by Enrichr on CodePen. Original code, written from scratch. |
Find this technique on a real site
Want to see this technique in the wild? Check any site with the free tools, then build your own version from this page.
Check which animation library a site uses · Extract a site's design tokens · Browse site breakdowns
Licence and provenance
Free to use in personal and client projects during the preview. Final licence terms will be published here before any paid tier starts; ask before reselling the code as a collection.
This is original code. The technique is a common one that we studied by measuring award-winning sites with Enrichr; no third-party code, shaders, fonts or images are used.
More elements
FAQ
How do I change the headline?
Use the Headline setting and put a | wherever you want a line break, or edit the h2 in the markup. Keep it to two or three short lines; the type is sized at 11.5% of the element's width.
Does it need a smooth-scroll library?
No. The original page used a Lenis-style smooth scroll. Here only the hero's own reaction is smoothed: each frame it moves 8.5% of the way toward the real scroll position, which gives the same soft follow without taking over page scrolling.
Can I use my own illustrations instead of hands?
Yes. Swap the path inside each .ef-ghh__hand SVG for your own drawing. Keep the bottom of the drawing at the bottom of the viewBox, because that is the point it sways around.
Why does the pointer dot disappear on phones?
Touch screens have no hover, so the dot and the hidden system cursor would only get in the way. A media query hides the dot when the main pointer is coarse.