Copied!
Navigation and lists

Rule sweep list

A line sweeps across a menu row on hover or focus and exits through the opposite side.

← Library

Element · Vanilla JS + CSSUpdated November 5, 2026

CursorElementVanilla 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 "Rule sweep list" for my site using the Enrichr Library element at https://enrichr.dev/elements/rule-sweep-list.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/rule-sweep-list.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:
- items: Work, Studio, Journal, Contact
- speed: 450
- color: #ffffff
- line: #c6f26b

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/rule-sweep-list.json.

Code

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

HTML

<ul class="ef-rule"></ul>

CSS

.ef-rule { list-style: none; margin: 0; padding: 0; }
.ef-rule__row { position: relative; display: flex; align-items: baseline; gap: 1rem; padding: 1rem 0; font-size: clamp(1.3rem, 3.4vw, 2.2rem); font-weight: 600; cursor: pointer; outline: none; }
.ef-rule__row::before { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: currentColor; opacity: 0.22; }
.ef-rule__row::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--ef-line, currentColor); transform: scaleX(0); transform-origin: right center; transition: transform var(--ef-dur, 450ms) cubic-bezier(0.7, 0, 0.2, 1); }
.ef-rule__row:hover::after, .ef-rule__row:focus-visible::after { transform: scaleX(1); transform-origin: left center; }
.ef-rule__n { font-size: 0.5em; opacity: 0.6; font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) { .ef-rule__row::after { transition: none; } }

JavaScript

// Rule sweep list: a line sweeps across a row on hover or keyboard focus, and exits the other side.
// mount(root, { items (comma separated), color, line (colour of the sweep), speed (ms) })
function mount(root, o) {
  var ul = root.querySelector(".ef-rule");
  var opts = {};
  function render() {
    ul.textContent = "";
    ul.style.color = opts.color;
    ul.style.setProperty("--ef-line", opts.line);
    ul.style.setProperty("--ef-dur", opts.speed + "ms");
    opts.items.split(",").map(function (s) { return s.trim(); }).filter(Boolean).forEach(function (label, i) {
      var li = document.createElement("li");
      li.className = "ef-rule__row";
      li.tabIndex = 0;
      var n = document.createElement("span");
      n.className = "ef-rule__n";
      n.textContent = String(i + 1).padStart(2, "0");
      var t = document.createElement("span");
      t.textContent = label;
      li.appendChild(n);
      li.appendChild(t);
      ul.appendChild(li);
    });
  }
  function update(next) {
    opts = Object.assign({ items: "Work, Studio, Journal, Contact", color: "#ffffff", line: "#c6f26b", speed: 450 }, opts, next || {});
    render();
  }
  update(o);
  return { update: update, destroy: function () {} };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"items":"Work, Studio, Journal, Contact","speed":450,"color":"#ffffff","line":"#c6f26b"});

How it works

The line is a pseudo-element scaled to zero with its origin on the right. On hover the scale goes to one and the origin flips to the left, so the line grows in from the left. When the pointer leaves, the origin is back on the right, so it shrinks away to the right: one continuous sweep, no JavaScript on hover.

Accessibility and performance

Rows are focusable and the sweep also runs on keyboard focus. Reduced motion turns the transition off, so the line simply appears.

A single transform transition per row, no layout work. About 0.8 KB of JavaScript, only used to build the rows.

SectionNavigation and lists
TechniquesPointer interaction
StackVanilla JS, CSS
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested

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.

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. Blend-mode cursorA custom cursor dot that inverts what is beneath it and grows over links, for fine pointers only.

FAQ

Why does the line leave on the other side?

The transform-origin changes between the hover and resting states. The element is scaling down from the right edge, which looks like the line travelling through.

Is it keyboard accessible?

Yes. Each row is focusable and :focus-visible triggers the same sweep.

Can the rows be links?

Yes. Swap the li content for anchors and keep the row class; the effect is on the row, not the content.