Copied!
Band

Edge fade logo strip

A small label over a row of word-marks that slides past endlessly and fades out at both edges, in a light and a dark version.

← Library

Element · Vanilla JS + CSSUpdated November 6, 2026

TextElementVanilla JS + CSS

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 "Edge fade logo strip" for my site using the Enrichr Library element at https://enrichr.dev/elements/edge-fade-logo-strip.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/edge-fade-logo-strip.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:
- label: Inside every capture
- words: Layout, Enrichr, Motion, Design tokens, shaders lab, Font Finder
- theme: both
- speed: 40
- gap: 5
- height: 34
- dim: 0.55
- fade: 90
- lightBg: #fbf8f1
- lightInk: #0c1e18
- darkBg: #0c1e18
- darkInk: #fbf8f1
- background: #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/edge-fade-logo-strip.json.

Code

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

HTML

<div class="ef-efl">
  <div class="ef-efl__strip ef-efl__strip--light" tabindex="0" role="group">
    <p class="ef-efl__label" aria-hidden="true"></p>
    <ul class="ef-efl__sr"></ul>
    <div class="ef-efl__track" aria-hidden="true"></div>
  </div>
  <div class="ef-efl__strip ef-efl__strip--dark" tabindex="0" role="group">
    <p class="ef-efl__label" aria-hidden="true"></p>
    <ul class="ef-efl__sr"></ul>
    <div class="ef-efl__track" aria-hidden="true"></div>
  </div>
</div>

CSS

/* Enrichr Library · https://enrichr.dev */
.ef-efl { --ef-efl-page: #fbf8f1; --ef-efl-light-bg: #fbf8f1; --ef-efl-light-ink: #0c1e18; --ef-efl-dark-bg: #0c1e18; --ef-efl-dark-ink: #fbf8f1; --ef-efl-gap: 5rem; --ef-efl-height: 34px; --ef-efl-k: 1; --ef-efl-dim: 0.55; --ef-efl-fade: 90px; position: relative; width: 100%; min-height: 20rem; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; gap: 60px; padding: 60px 0; overflow: hidden; border-radius: 1rem; background: var(--ef-efl-page); font-family: 'Parkinsans', 'Space Grotesk', sans-serif; }
.ef-efl .ef-efl__strip { --ef-efl-ink: var(--ef-efl-light-ink); --ef-efl-bg: var(--ef-efl-light-bg); --ef-efl-d: var(--ef-efl-dim); position: relative; width: 100%; box-sizing: border-box; overflow: hidden; padding: 28px 0; background: var(--ef-efl-bg); border-top: 1px solid color-mix(in srgb, var(--ef-efl-ink) 10%, var(--ef-efl-bg)); border-bottom: 1px solid color-mix(in srgb, var(--ef-efl-ink) 10%, var(--ef-efl-bg)); outline: none; -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--ef-efl-fade), #000 calc(100% - var(--ef-efl-fade)), transparent 100%); mask-image: linear-gradient(to right, transparent 0, #000 var(--ef-efl-fade), #000 calc(100% - var(--ef-efl-fade)), transparent 100%); }
.ef-efl .ef-efl__strip--dark { --ef-efl-ink: var(--ef-efl-dark-ink); --ef-efl-bg: var(--ef-efl-dark-bg); --ef-efl-d: calc(var(--ef-efl-dim) + 0.05); border-color: color-mix(in srgb, var(--ef-efl-ink) 13%, var(--ef-efl-bg)); }
.ef-efl .ef-efl__strip:focus-visible { box-shadow: inset 0 0 0 3px #e6ff55, inset 0 0 0 5px var(--ef-efl-bg); }
.ef-efl.is-light .ef-efl__strip--dark, .ef-efl.is-dark .ef-efl__strip--light { display: none; }
.ef-efl .ef-efl__label { display: block; margin: 0 0 22px; padding: 0 1rem; text-align: center; font-size: 11px; font-weight: 500; line-height: 1.3; letter-spacing: 0.18em; text-transform: uppercase; color: color-mix(in srgb, var(--ef-efl-ink) 45%, var(--ef-efl-bg)); opacity: 1; }
.ef-efl .ef-efl__strip--dark .ef-efl__label { color: color-mix(in srgb, var(--ef-efl-ink) 42%, var(--ef-efl-bg)); }
.ef-efl .ef-efl__track { display: flex; width: max-content; will-change: transform; }
.ef-efl .ef-efl__group { display: flex; flex: 0 0 auto; align-items: center; gap: var(--ef-efl-gap); padding-right: var(--ef-efl-gap); }
.ef-efl .ef-efl__logo { display: flex; align-items: center; justify-content: center; height: var(--ef-efl-height); color: var(--ef-efl-ink); opacity: var(--ef-efl-d); white-space: nowrap; user-select: none; transition: opacity 0.25s ease; }
.ef-efl .ef-efl__logo:hover { opacity: 1; }
.ef-efl .ef-efl__wm { display: flex; align-items: center; line-height: 1; }
.ef-efl .ef-efl__wm--serif { font-family: Georgia, 'Times New Roman', serif; font-weight: 700; font-size: calc(30px * var(--ef-efl-k)); letter-spacing: -0.015em; }
.ef-efl .ef-efl__wm--mark { font-weight: 700; font-size: calc(24px * var(--ef-efl-k)); letter-spacing: -0.01em; gap: calc(9px * var(--ef-efl-k)); }
.ef-efl .ef-efl__mark { width: calc(24px * var(--ef-efl-k)); height: calc(24px * var(--ef-efl-k)); flex: 0 0 auto; }
.ef-efl .ef-efl__wm--caps { font-weight: 700; font-size: calc(23px * var(--ef-efl-k)); letter-spacing: -0.03em; text-transform: uppercase; }
.ef-efl .ef-efl__wm--serifcaps { font-family: Georgia, 'Times New Roman', serif; font-weight: 700; font-size: calc(21px * var(--ef-efl-k)); letter-spacing: 0.02em; text-transform: uppercase; }
.ef-efl .ef-efl__wm--sub { font-weight: 700; font-size: calc(25px * var(--ef-efl-k)); letter-spacing: -0.035em; text-transform: lowercase; }
.ef-efl .ef-efl__sub { margin-left: 7px; align-self: flex-end; padding-bottom: 2px; font-weight: 400; font-size: 0.62em; letter-spacing: 0.01em; }
.ef-efl .ef-efl__wm--box { font-weight: 700; font-size: calc(23px * var(--ef-efl-k)); letter-spacing: -0.02em; gap: calc(7px * var(--ef-efl-k)); }
.ef-efl .ef-efl__box { display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; width: calc(26px * var(--ef-efl-k)); height: calc(26px * var(--ef-efl-k)); border: 2.5px solid currentColor; font-size: calc(15px * var(--ef-efl-k)); letter-spacing: 0; flex: 0 0 auto; }
.ef-efl .ef-efl__sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; list-style: none; }
.ef-efl.is-still .ef-efl__strip { -webkit-mask-image: none; mask-image: none; }
.ef-efl.is-still .ef-efl__track { width: auto; justify-content: center; }
.ef-efl.is-still .ef-efl__group { flex: 1 1 auto; flex-wrap: wrap; justify-content: center; row-gap: 2rem; padding: 0 1rem; }

JavaScript

// Enrichr Library · https://enrichr.dev
// Edge fade logo strip: a small caps label over a row of typographic word-marks that slides past on an endless linear
// loop, with both ends faded out by a mask instead of a hard cut. The marks rest dimmed and come up to full strength under
// the pointer, and the whole strip pauses while the pointer is over it or it has keyboard focus. The row is two identical
// groups and the track slides by exactly one group (50%) every 40 seconds, so the loop has no seam; on a wide screen each
// group repeats its set and the loop time grows with it, so the pace stays the same. It comes in a light and a dark version.
// The loop is a Web Animation on the compositor. Reduced motion stops it, drops the fade and wraps the marks centred.
// It pauses off screen and in hidden tabs.
// mount(root, { label, words (comma separated; "main sub" on the fifth style), theme ("both", "light" or "dark"),
//               speed (s per loop), gap (rem), height (px), dim (resting opacity), fade (edge fade, px),
//               lightBg, lightInk, darkBg, darkInk, background })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var box = root.querySelector(".ef-efl");
  var MARK = '<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"/>';
  var STYLES = ["serif", "mark", "caps", "serifcaps", "sub", "box"]; // one per word, in turn, like the six press marks of the original
  var strips = [].map.call(root.querySelectorAll(".ef-efl__strip"), function (el) {
    return { el: el, label: el.querySelector(".ef-efl__label"), sr: el.querySelector(".ef-efl__sr"), track: el.querySelector(".ef-efl__track"), anim: null, dur: 0, k: 1, hover: false, focus: false, sig: "" };
  });
  var opts = {}, io = null, ro = null, visible = true, destroyed = false, lastW = 0, offs = [];

  function num(v, d) { v = Number(v); return isFinite(v) ? v : d; }
  function words() {
    var w = String(opts.words).split(",").map(function (s) { return s.trim(); }).filter(Boolean).slice(0, 12);
    return w.length ? w : ["Enrichr"];
  }
  function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text) e.textContent = text; return e; }
  function initials(w) {
    var p = w.split(/\s+/).filter(Boolean);
    return (p.length > 1 ? p[0].charAt(0) + p[1].charAt(0) : w.slice(0, 2)).toUpperCase();
  }
  function logo(w, i) {
    var kind = STYLES[i % STYLES.length], slot = el("div", "ef-efl__logo"), wm = el("span", "ef-efl__wm ef-efl__wm--" + kind);
    if (kind === "mark") {
      var s = document.createElementNS("http://www.w3.org/2000/svg", "svg");
      s.setAttribute("viewBox", "0 0 50 50"); s.setAttribute("class", "ef-efl__mark"); s.setAttribute("aria-hidden", "true"); s.setAttribute("focusable", "false");
      s.innerHTML = MARK;
      wm.appendChild(s); wm.appendChild(document.createTextNode(w));
    } else if (kind === "sub") {
      var k = w.lastIndexOf(" ");
      wm.appendChild(document.createTextNode(k > 0 ? w.slice(0, k) : w));
      if (k > 0) wm.appendChild(el("span", "ef-efl__sub", w.slice(k + 1)));
    } else if (kind === "box") {
      wm.appendChild(el("span", "ef-efl__box", initials(w)));
      wm.appendChild(document.createTextNode(w));
    } else wm.textContent = w;
    slot.appendChild(wm);
    return slot;
  }
  function groupOf(list, sets) {
    var g = el("div", "ef-efl__group");
    for (var s = 0; s < sets; s++) list.forEach(function (w, i) { g.appendChild(logo(w, i)); });
    return g;
  }
  function paused(st) { return !visible || st.hover || st.focus || document.hidden; }
  function sync() { strips.forEach(function (st) { if (st.anim) { if (paused(st)) st.anim.pause(); else st.anim.play(); } }); }

  // Restart one strip's loop at its current place, so live changes never jump.
  function play(st, sec) {
    if (reduce || !st.track.animate) return;
    var dur = Math.max(1, sec) * 1000, p = 0;
    if (st.anim) { p = st.dur ? ((Number(st.anim.currentTime) || 0) % st.dur) / st.dur : 0; st.anim.cancel(); }
    st.anim = st.track.animate([{ transform: "translate3d(0,0,0)" }, { transform: "translate3d(-50%,0,0)" }], { duration: dur, iterations: Infinity, easing: "linear" });
    st.anim.currentTime = p * dur;
    st.dur = dur;
    if (paused(st)) st.anim.pause();
  }
  // One group must be at least as wide as the strip; repeat the set inside it until it is.
  function build(st) {
    var list = words();
    st.label.textContent = opts.label;
    st.el.setAttribute("aria-label", (opts.label ? opts.label + ". " : "") + "Focus to pause the strip.");
    st.sr.textContent = "";
    list.forEach(function (w) { st.sr.appendChild(el("li", "", w)); });
    st.track.textContent = "";
    var probe = groupOf(list, 1);
    st.track.appendChild(probe);
    var one = probe.getBoundingClientRect().width || 600;
    st.k = reduce ? 1 : Math.max(1, Math.ceil((box.clientWidth || 800) / one));
    st.track.textContent = "";
    st.track.appendChild(groupOf(list, st.k));
    if (!reduce) st.track.appendChild(groupOf(list, st.k)); // the exact duplicate that makes the loop seamless
    st.sig = list.join(",") + "|" + opts.label + "|" + opts.gap + "|" + opts.height;
  }
  function visibleStrips() { return strips.filter(function (st, i) { return opts.theme === "both" || (opts.theme === "light") === (i === 0); }); }

  function update(partial) {
    opts = Object.assign({
      label: "Inside every capture", words: "Layout, Enrichr, Motion, Design tokens, shaders lab, Font Finder", theme: "both",
      speed: 40, gap: 5, height: 34, dim: 0.55, fade: 90,
      lightBg: "#fbf8f1", lightInk: "#0c1e18", darkBg: "#0c1e18", darkInk: "#fbf8f1", background: "#fbf8f1"
    }, opts, partial || {});
    opts.speed = Math.max(1, num(opts.speed, 40)); opts.gap = Math.max(0, num(opts.gap, 5)); opts.height = Math.max(10, num(opts.height, 34));
    opts.dim = Math.min(1, Math.max(0, num(opts.dim, 0.55))); opts.fade = Math.max(0, num(opts.fade, 90));
    if (["both", "light", "dark"].indexOf(opts.theme) < 0) opts.theme = "both";
    var s = box.style;
    s.setProperty("--ef-efl-page", opts.background);
    s.setProperty("--ef-efl-light-bg", opts.lightBg); s.setProperty("--ef-efl-light-ink", opts.lightInk);
    s.setProperty("--ef-efl-dark-bg", opts.darkBg); s.setProperty("--ef-efl-dark-ink", opts.darkInk);
    s.setProperty("--ef-efl-gap", opts.gap + "rem"); s.setProperty("--ef-efl-height", opts.height + "px");
    s.setProperty("--ef-efl-k", String(opts.height / 34)); s.setProperty("--ef-efl-dim", String(opts.dim));
    s.setProperty("--ef-efl-fade", opts.fade + "px");
    box.classList.toggle("is-light", opts.theme === "light");
    box.classList.toggle("is-dark", opts.theme === "dark");
    box.classList.toggle("is-still", reduce);
    if (destroyed) return;
    strips.forEach(function (st) { if (visibleStrips().indexOf(st) < 0) { if (st.anim) st.anim.cancel(); st.anim = null; st.sig = ""; } });
    visibleStrips().forEach(function (st) {
      if (st.sig !== words().join(",") + "|" + opts.label + "|" + opts.gap + "|" + opts.height) build(st);
      play(st, opts.speed * st.k);
    });
    lastW = box.clientWidth;
  }
  function refit() { if (destroyed) return; visibleStrips().forEach(function (st) { build(st); play(st, opts.speed * st.k); }); lastW = box.clientWidth; }

  strips.forEach(function (st) {
    function enter() { st.hover = true; sync(); }
    function leave() { st.hover = false; sync(); }
    function fin() { st.focus = true; sync(); }
    function fout() { st.focus = st.el.contains(document.activeElement); sync(); }
    st.el.addEventListener("pointerenter", enter); st.el.addEventListener("pointerleave", leave);
    st.el.addEventListener("focusin", fin); st.el.addEventListener("focusout", fout);
    offs.push(function () {
      st.el.removeEventListener("pointerenter", enter); st.el.removeEventListener("pointerleave", leave);
      st.el.removeEventListener("focusin", fin); st.el.removeEventListener("focusout", fout);
    });
  });
  function onVis() { sync(); }
  document.addEventListener("visibilitychange", onVis);

  update(o);
  if ("IntersectionObserver" in window) { io = new IntersectionObserver(function (e) { visible = e[0].isIntersecting; sync(); }); io.observe(box); }
  if ("ResizeObserver" in window) { ro = new ResizeObserver(function () { if (Math.abs(box.clientWidth - lastW) > 24) refit(); }); ro.observe(box); }
  if (document.fonts && document.fonts.ready) document.fonts.ready.then(function () { refit(); });
  return {
    update: update,
    destroy: function () {
      destroyed = true;
      strips.forEach(function (st) { if (st.anim) st.anim.cancel(); st.anim = null; });
      offs.forEach(function (f) { f(); }); offs = [];
      document.removeEventListener("visibilitychange", onVis);
      if (io) io.disconnect();
      if (ro) ro.disconnect();
    }
  };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"label":"Inside every capture","words":"Layout, Enrichr, Motion, Design tokens, shaders lab, Font Finder","theme":"both","speed":40,"gap":5,"height":34,"dim":0.55,"fade":90,"lightBg":"#fbf8f1","lightInk":"#0c1e18","darkBg":"#0c1e18","darkInk":"#fbf8f1","background":"#fbf8f1"});

How it works

Each strip is a label and a track holding two identical groups of word-marks. The track slides by minus 50 percent of its width, exactly one group, every 40 seconds on a linear loop, so the end frame matches the start. Every word gets one of six type treatments in turn, like a row of press logos: a bold serif, the Enrichr puzzle mark beside bold sans, tight sans capitals, spaced serif capitals, lowercase with a small trailing word, and a boxed pair of initials. A CSS mask fades both ends to transparent over 90 pixels instead of cutting them off. Marks rest at 55 percent opacity (60 on the dark strip) and come up to full in a quarter of a second under the pointer, and the strip pauses while the pointer is on it or it has focus. If one group is narrower than the strip, the set repeats inside each group and the loop time grows to match, so the pace stays the same on wide screens.

Accessibility and performance

The label and the word-marks are read once, as a group label and a visually hidden list; the moving copies are hidden from screen readers. Each strip can take keyboard focus, which pauses it and shows a focus ring. With prefers-reduced-motion the strip stops, the edge fade goes, and the marks wrap onto centred rows.

The loop is one Web Animation per strip that only moves transform, so it runs on the compositor with no JavaScript per frame. The marks are measured once, after web fonts load, and when the width changes by more than 24 pixels. Strips pause off screen through IntersectionObserver and in hidden tabs, and a strip that is switched off is not animated at all.

SectionBand
TechniquesText animation, Pointer interaction
StackVanilla JS, CSS
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
On CodePenOpen the live pen on our CodePen account; fork it to experiment.
Inspired byPress ticker 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.

Hover font variantsA text index whose rows flip through serif, mono, outline, wide and italic styles while hovered or focused. Distorted link hoverBig links whose letters and underline ripple like heat haze on hover, using an animated SVG turbulence displacement filter. Gooey text hoverA menu of big words with a gooey blob that stretches and follows the pointer from row to row while the active word turns bold.

FAQ

How is this different from the Marquee ticker element?

Marquee ticker is one large line of text. This is a logo strip: a small caps label, mixed word-mark styles that brighten under the pointer, soft faded edges, and ready-made light and dark versions.

Can I use real logo files?

Yes. Replace the word-mark inside each .ef-efl__logo with an img or inline SVG set to height: 100%, and do it in both groups so the loop stays seamless. Only show logos you have permission to use.

How do I turn off the edge fade?

Set Edge fade to 0. The mask then runs edge to edge, which is the same as a hard cut.

Why does it pause on hover?

So people can read a mark or click one if you make them links. It also pauses when it has keyboard focus, so anyone can stop the movement.