Copied!
Background

Hyperspeed light streaks

Light streaks rush toward the viewer with perspective on a canvas; the pointer steers the vanishing point and a click boosts the speed.

← Library

Element · Vanilla JS + Canvas 2D + SVGUpdated November 6, 2026

BackgroundElementVanilla JS + Canvas 2D + SVG

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 "Hyperspeed light streaks" for my site using the Enrichr Library element at https://enrichr.dev/elements/hyperspeed-lines.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/hyperspeed-lines.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:
- accent: #e6ff55
- speed: 1
- count: 220
- length: 0.25
- background: #0c1e18

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/hyperspeed-lines.json.

Code

Mount it on an element that contains the markup. It returns an object with update(options) and destroy().

HTML

<div class="ef-hsl">
  <div class="ef-hsl__wrap">
    <canvas class="ef-hsl__cv" aria-hidden="true"></canvas>
    <svg class="ef-hsl__art" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false">
      <defs>
        <symbol id="ef-hsl-pz1" viewBox="1513.7 1453.9 322.3 322.3"><path fill="currentColor" d="M1821 1761.2h-292.3v-111.6c0-8 6.5-14.5 14.5-14.5 4.2 0 8.2 1.9 11 5.1 7.8 8.6 19.7 13.6 32.7 11.9 16.8-2.3 30.1-16 32-32.7 2.5-22.6-15.1-41.6-37.1-41.6-11 0-20.9 4.7-27.7 12.3-2.8 3.1-6.7 4.9-10.9 4.9-8 0-14.5-6.4-14.5-14.4v-111.7h292.3v111.7c0 8-6.4 14.4-14.5 14.4-4.2 0-8.1-1.8-10.9-5-7.9-8.6-19.7-13.6-32.7-11.9-16.8 2.2-30.1 15.9-32 32.7-2.5 22.6 15.1 41.6 37.1 41.6 11 0 20.8-4.8 27.7-12.3 2.8-3.1 6.7-5 10.8-5 8.1 0 14.5 6.5 14.5 14.5z"/></symbol>
        <symbol id="ef-hsl-pz4" viewBox="1423.36 860.8 503.07 322.3"><path fill="currentColor" d="M1821 875.8v111.6c0 8 6.5 14.5 14.5 14.5 4.2 0 8.1-1.9 10.9-5 6.8-7.5 16.7-12.3 27.7-12.3 22 0 39.6 19 37.1 41.6-1.9 16.7-15.2 30.5-32 32.7-13 1.7-24.8-3.3-32.7-11.9-2.8-3.2-6.8-5-11-5-8 0-14.5 6.4-14.5 14.4v111.7h-111.6c-8 0-14.5-6.5-14.5-14.5 0-4.2 1.9-8.2 5-11 8.7-7.9 13.7-19.7 12-32.7-2.3-16.8-16-30.1-32.8-32-22.5-2.5-41.5 15.1-41.5 37.1 0 11 4.7 20.9 12.3 27.7 3.1 2.8 4.9 6.7 4.9 10.8v0.1c0 8-6.4 14.5-14.5 14.5h-111.6v-111.7c0-8-6.4-14.4-14.5-14.4-4.1 0-8 1.8-10.8 4.9-6.9 7.6-16.7 12.3-27.7 12.3-22 0-39.6-19-37.1-41.6 1.9-16.7 15.2-30.4 32-32.7 13-1.7 24.8 3.3 32.7 11.9 2.8 3.2 6.7 5.1 10.9 5.1 8.1 0 14.5-6.5 14.5-14.5v-111.6z"/></symbol>
        <symbol id="ef-hsl-pz8" viewBox="1513.7 166.86 322.3 503.06"><path fill="currentColor" d="M1528.7 564.5v-111.6c0-8 6.5-14.5 14.5-14.5 4.2 0 8.2 1.9 11 5 7.8 8.7 19.7 13.7 32.7 11.9 16.8-2.2 30.1-15.9 32-32.7 2.5-22.5-15.1-41.5-37.1-41.5-11 0-20.9 4.7-27.7 12.3-2.8 3.1-6.7 4.9-10.9 4.9-8 0-14.5-6.5-14.5-14.5v-111.6h111.6c8.1 0 14.5-6.5 14.5-14.5 0-4.1-1.8-8.1-4.9-10.9-7.6-6.8-12.3-16.6-12.3-27.6 0-22 19-39.6 41.5-37.1 16.8 1.8 30.5 15.2 32.8 32 1.7 12.9-3.3 24.8-12 32.7-3.1 2.8-5 6.7-5 10.9 0 8 6.5 14.5 14.5 14.5h111.6v111.6c0 8-6.4 14.5-14.5 14.5-4.2 0-8.1-1.9-10.9-5-7.9-8.7-19.7-13.6-32.7-11.9-16.8 2.2-30.1 15.9-32 32.7-2.5 22.5 15.1 41.6 37.1 41.6 11 0 20.8-4.8 27.7-12.3 2.8-3.1 6.7-5 10.8-5 8.1 0 14.5 6.5 14.5 14.5v111.6h-111.6c-8 0-14.5 6.5-14.5 14.5 0 4.2 1.9 8.1 5 10.9 7.5 6.8 12.3 16.7 12.3 27.7 0 22-19 39.5-41.6 37.1-16.8-1.9-30.5-15.3-32.7-32-1.7-13 3.3-24.9 11.9-32.7 3.1-2.9 5-6.8 5-11 0-8-6.4-14.5-14.5-14.5z"/></symbol>
      </defs>
      <use class="ef-hsl__pz ef-hsl__pz--a" href="#ef-hsl-pz1" x="40" y="190" width="70" height="70"/>
      <use class="ef-hsl__pz ef-hsl__pz--b" href="#ef-hsl-pz4" x="262" y="40" width="96" height="62"/>
      <use class="ef-hsl__pz ef-hsl__pz--c" href="#ef-hsl-pz8" x="300" y="170" width="52" height="82"/>
    </svg>
  </div>
</div>

CSS

/* Enrichr Library · https://enrichr.dev */
.ef-hsl { width: 100%; }
.ef-hsl__wrap { position: relative; box-sizing: border-box; width: 100%; min-height: 18rem; aspect-ratio: 16 / 10; overflow: hidden; border-radius: 1rem; background: #0c1e18; isolation: isolate; }
.ef-hsl__cv { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.ef-hsl__art { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
.ef-hsl__pz { color: #fbf8f1; opacity: 0.9; transform-box: fill-box; transform-origin: 50% 50%; animation: ef-hsl-float 7s ease-in-out infinite; filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.45)); }
.ef-hsl__pz--a { color: var(--ef-acc, #e6ff55); animation-duration: 6s; }
.ef-hsl__pz--b { color: #fbf8f1; animation-duration: 8s; animation-delay: -2s; opacity: 0.8; }
.ef-hsl__pz--c { color: #2f7d5b; animation-duration: 7s; animation-delay: -4s; }
@keyframes ef-hsl-float { 0%, 100% { transform: translateY(0) rotate(-6deg); } 50% { transform: translateY(-9px) rotate(5deg); } }
@media (prefers-reduced-motion: reduce) { .ef-hsl__pz { animation: none; } }

JavaScript

// Enrichr Library · https://enrichr.dev
// Hyperspeed lines: random light streaks rush toward the viewer on a 2D canvas using a simple perspective projection.
// The pointer steers the vanishing point, a click or tap boosts speed for a moment. Original rebuild from the general idea.
// Reduced motion draws one still frame. Pauses off-screen. mount(root, { background, accent, speed (0.2-3), count (40-400), length (0.05-0.6) })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var wrap = root.querySelector(".ef-hsl__wrap");
  var cv = root.querySelector(".ef-hsl__cv");
  var ctx = cv.getContext("2d");
  var opts = {}, W = 0, H = 0, dpr = 1, streaks = [], raf = 0, last = 0, visible = true;
  var vp = { x: 0.5, y: 0.5 }, target = { x: 0.5, y: 0.5 }, boost = 0, offs = [];
  var PAL = ["#e6ff55", "#c6f26b", "#fbf8f1", "#2f7d5b", "#0a6b46"];

  function spawn(s, far) {
    var a = Math.random() * Math.PI * 2, r = 0.08 + Math.pow(Math.random(), 0.7) * 1.4;
    s.x = Math.cos(a) * r; s.y = Math.sin(a) * r;
    s.z = far ? 0.2 + Math.random() * 0.9 : 1 + Math.random() * 0.1;
    s.c = Math.random() < 0.45 ? 0 : 1 + Math.floor(Math.random() * (PAL.length - 1));
    s.w = 0.6 + Math.random() * 1.6;
    s.v = 0.7 + Math.random() * 0.6;
    return s;
  }
  function build() {
    streaks = [];
    for (var i = 0; i < opts.count; i++) streaks.push(spawn({}, true));
  }
  function size() {
    var r = wrap.getBoundingClientRect();
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    W = Math.max(1, Math.round(r.width)); H = Math.max(1, Math.round(r.height));
    cv.width = W * dpr; cv.height = H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    draw();
  }
  function proj(x, y, z, f) { return [vp.x * W + (x / z) * f, vp.y * H + (y / z) * f]; }
  function draw() {
    ctx.globalCompositeOperation = "source-over";
    ctx.globalAlpha = 1;
    ctx.fillStyle = opts.background;
    ctx.fillRect(0, 0, W, H);
    var f = Math.max(W, H) * 0.32;
    var g = ctx.createRadialGradient(vp.x * W, vp.y * H, 0, vp.x * W, vp.y * H, Math.max(W, H) * 0.5);
    g.addColorStop(0, "rgba(10,107,70,0.55)"); g.addColorStop(1, "rgba(10,107,70,0)");
    ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
    ctx.lineCap = "round";
    ctx.globalCompositeOperation = "lighter";
    var tl = opts.length * (1 + boost * 1.6);
    for (var i = 0; i < streaks.length; i++) {
      var s = streaks[i];
      var a = proj(s.x, s.y, s.z, f), b = proj(s.x, s.y, s.z + tl * s.z, f);
      var near = 1 - Math.min(1, s.z);
      ctx.globalAlpha = Math.min(1, 0.15 + near * 1.1) * Math.min(1, (1.1 - s.z) * 8 + 0.1);
      ctx.strokeStyle = s.c === 0 ? opts.accent : PAL[s.c];
      ctx.lineWidth = s.w * (0.5 + near * 2.2);
      ctx.beginPath(); ctx.moveTo(b[0], b[1]); ctx.lineTo(a[0], a[1]); ctx.stroke();
    }
    ctx.globalAlpha = 1;
  }
  function step(dt) {
    vp.x += (target.x - vp.x) * Math.min(1, dt * 4);
    vp.y += (target.y - vp.y) * Math.min(1, dt * 4);
    boost *= Math.pow(0.04, dt);
    var sp = opts.speed * (0.35 + boost * 2.6) * dt;
    for (var i = 0; i < streaks.length; i++) {
      var s = streaks[i];
      s.z -= sp * s.v * (0.4 + (1 - s.z) * 1.4);
      var p = proj(s.x, s.y, s.z, Math.max(W, H) * 0.32);
      if (s.z < 0.04 || p[0] < -80 || p[0] > W + 80 || p[1] < -80 || p[1] > H + 80) spawn(s, false);
    }
  }
  function loop(t) {
    raf = 0;
    if (!visible) return;
    var dt = Math.min(0.05, (t - last) / 1000 || 0.016); last = t;
    step(dt); draw();
    raf = requestAnimationFrame(loop);
  }
  function play() { if (!reduce && visible && !raf) { last = performance.now(); raf = requestAnimationFrame(loop); } }
  function pmove(e) {
    var r = wrap.getBoundingClientRect();
    target.x = 0.5 + ((e.clientX - r.left) / r.width - 0.5) * 0.7;
    target.y = 0.5 + ((e.clientY - r.top) / r.height - 0.5) * 0.7;
    if (reduce) { vp.x = target.x; vp.y = target.y; draw(); }
  }
  function pleave() { target.x = 0.5; target.y = 0.5; if (reduce) { vp.x = 0.5; vp.y = 0.5; draw(); } }
  function pdown() { boost = 1; }
  wrap.addEventListener("pointermove", pmove);
  wrap.addEventListener("pointerleave", pleave);
  wrap.addEventListener("pointerdown", pdown);
  offs.push(function () { wrap.removeEventListener("pointermove", pmove); wrap.removeEventListener("pointerleave", pleave); wrap.removeEventListener("pointerdown", pdown); });
  var ro = null, io = null;
  if (window.ResizeObserver) { ro = new ResizeObserver(size); ro.observe(wrap); offs.push(function () { ro.disconnect(); }); }
  else { window.addEventListener("resize", size); offs.push(function () { window.removeEventListener("resize", size); }); }
  if (window.IntersectionObserver) {
    io = new IntersectionObserver(function (en) {
      visible = en[en.length - 1].isIntersecting;
      if (visible) play(); else if (raf) { cancelAnimationFrame(raf); raf = 0; }
    });
    io.observe(wrap); offs.push(function () { io.disconnect(); });
  }
  function update(next) {
    var prev = opts;
    opts = Object.assign({ background: "#0c1e18", accent: "#e6ff55", speed: 1, count: 220, length: 0.25 }, opts, next || {});
    opts.count = Math.max(20, Math.min(500, Math.round(opts.count)));
    wrap.style.background = opts.background;
    wrap.style.setProperty("--ef-acc", opts.accent);
    if (prev.count !== opts.count || !streaks.length) {
      build();
      if (reduce) step(0.3);
    }
    if (W) draw();
  }
  update(o);
  size();
  play();
  return {
    update: update,
    destroy: function () {
      if (raf) cancelAnimationFrame(raf);
      raf = 0; visible = false;
      offs.forEach(function (f) { f(); }); offs = [];
    }
  };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"accent":"#e6ff55","speed":1,"count":220,"length":0.25,"background":"#0c1e18"});

How it works

Each streak is a point in 3D: x and y around the centre and a depth z between 0 and 1. Every frame z shrinks, and the point is projected to the screen as centre + (x or y) divided by z, so streaks start near the vanishing point and accelerate outward. A streak is drawn as a line from its projected position to the projection of a slightly deeper point, which gives the motion-blur length. Streaks that leave the frame or reach the viewer respawn far away. The vanishing point eases toward the pointer, and a pointer down sets a boost value that raises speed and length, then decays. Drawing uses additive blending so overlaps glow. Three Enrichr puzzle pieces float above in an inline SVG layer.

Accessibility and performance

The canvas and puzzle art are decorative and hidden from assistive tech. Reduced motion renders one still frame, with no animation or float. Rapid streaking can be uncomfortable for some people, so keep the speed low and offer a way to pause it if you use it behind content. Keep text above it high contrast.

One canvas, one path per streak per frame, so cost scales with the Streaks control. The canvas is capped at 2x pixel ratio, resized through a ResizeObserver, and the loop stops when the element is off-screen. The puzzle float is a CSS transform animation.

SectionBackground
TechniquesPointer interaction
StackVanilla JS, Canvas 2D, SVG
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
On CodePenOpen the live pen on our CodePen account; fork it to experiment.
Inspired byHyperspeed (React Bits) by React Bits on reactbits.dev. 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.

Rule sweep listA line sweeps across a menu row on hover or focus and exits through the opposite side. Depth parallax cardLayered shapes shift by different amounts as the pointer moves, drifting on touch screens. 3D ring carouselCards on a CSS 3D ring that rotates one step at a time, with buttons, arrow keys and autoplay.

FAQ

How do I make it faster or calmer?

Use the Speed control, or pass speed in update(). A click adds a temporary boost on top of it.

Can it sit behind a hero headline?

Yes. Put the mount element behind your content with a lower z-index and keep the text cream or white for contrast.

Does it need WebGL?

No. It is canvas 2D with a simple perspective divide, so it runs anywhere canvas does.