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 "Barber pole pattern" for my site using the Enrichr Library element at https://enrichr.dev/elements/barber-pole-pattern.html.
Read the machine-readable spec and source at https://enrichr.dev/elements/barber-pole-pattern.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:
- autoplay: 1
- sweep: 5
- tile: 120
- angle: 45
- background: #0c1e18
- stripe: #e6ff55
- band: #1a7a4c
- edge: #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/barber-pole-pattern.json.
Code
Mount it on an element that contains the markup. It returns an object with update(options) and destroy().
HTML
<div class="ef-bpp">
<svg class="ef-bpp__svg" viewBox="0 0 600 300" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Diagonal barber-pole stripes in lime, cream and green on a dark ground that slide along their angle">
<defs>
<pattern class="ef-bpp__pat" id="ef-bpp-pattern" width="60" height="60" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
<rect class="ef-bpp__bg" width="60" height="60"/>
<g class="ef-bpp__group">
<path class="ef-bpp__a" d="M-60,0 L0,0 L0,60 L-60,60 Z"/>
<path class="ef-bpp__c" d="M0,0 L30,0 L0,30 Z"/>
<path class="ef-bpp__b" d="M30,0 L60,0 L0,60 L0,30 Z"/>
<path class="ef-bpp__c" d="M0,60 L30,60 L0,90 Z"/>
<path class="ef-bpp__b" d="M30,60 L60,60 L60,90 L0,120 L0,90 Z"/>
<path class="ef-bpp__a" d="M60,0 L120,0 L120,60 L60,60 Z"/>
<path class="ef-bpp__c" d="M60,60 L90,60 L60,90 Z"/>
<path class="ef-bpp__b" d="M90,60 L120,60 L60,120 L60,90 Z"/>
</g>
</pattern>
</defs>
<rect class="ef-bpp__fill" width="100%" height="100%" fill="url(#ef-bpp-pattern)"/>
</svg>
</div>CSS
/* Enrichr Library · https://enrichr.dev */
.ef-bpp { --ef-bpp-bg: #0c1e18; --ef-bpp-a: #e6ff55; --ef-bpp-b: #1a7a4c; --ef-bpp-c: #fbf8f1; position: relative; width: 100%; min-height: 22rem; overflow: hidden; border-radius: 1rem; background: var(--ef-bpp-bg); }
.ef-bpp .ef-bpp__svg { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }
.ef-bpp .ef-bpp__bg { fill: var(--ef-bpp-bg); }
.ef-bpp .ef-bpp__a { fill: var(--ef-bpp-a); }
.ef-bpp .ef-bpp__b { fill: var(--ef-bpp-b); }
.ef-bpp .ef-bpp__c { fill: var(--ef-bpp-c); }JavaScript
// Enrichr Library · https://enrichr.dev
// Barber pole pattern: an SVG pattern tile (60 units square, turned 45 degrees) holding wide main stripes, thin light
// wedges and second-colour bands. A group inside the tile slides by (-60, +60) units as progress runs from 0 to 1, with no
// easing, so the stripes glide out along the diagonal and the tile clips them as they go; at the very end the tile is bare.
// With autoplay off, progress is scrubbed by scroll: 0 when the element's top enters at the bottom of the window, 1 when its
// bottom leaves at the top (a ScrollTrigger scrub, done with getBoundingClientRect on a passive scroll listener).
// With autoplay on, progress runs 0 to 1 and back in a straight line over the sweep time, like scrolling down and up again.
// Reduced motion shows one still frame at 30% progress and ignores scroll. Autoplay pauses off screen and in hidden tabs.
// mount(root, { autoplay ("1" = runs by itself, "0" = follows scroll), sweep (s for 0 to 1), tile (tile size, px),
// angle (deg), background, stripe (main stripe), band (second stripe), edge (thin wedges) })
function mount(root, o) {
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var box = root.querySelector(".ef-bpp");
var svg = root.querySelector(".ef-bpp__svg");
var pat = root.querySelector(".ef-bpp__pat");
var group = root.querySelector(".ef-bpp__group");
var fillRect = root.querySelector(".ef-bpp__fill");
var id = "ef-bpp-" + Math.random().toString(36).slice(2, 9); // each copy on a page needs its own pattern id
var opts = {}, raf = 0, sraf = 0, visible = true, io = null, ro = null, destroyed = false, last = 0, clock = 0, W = 1, H = 1;
pat.setAttribute("id", id);
fillRect.setAttribute("fill", "url(#" + id + ")");
function num(v, d) { v = Number(v); return isFinite(v) ? v : d; }
function render(p) {
p = p < 0 ? 0 : p > 1 ? 1 : p;
group.setAttribute("transform", "translate(" + (-60 * p).toFixed(3) + " " + (60 * p).toFixed(3) + ")");
}
function scrollProgress() {
var r = box.getBoundingClientRect(), vh = window.innerHeight || document.documentElement.clientHeight || 800;
return (vh - r.top) / (vh + r.height);
}
// linear there-and-back: 0 -> 1 over one sweep, then 1 -> 0 over the next
function autoProgress() { var t = (clock / opts.sweep) % 2; return t > 1 ? 2 - t : t; }
function draw() {
if (reduce) render(0.3);
else if (opts.autoplay) render(autoProgress());
else render(scrollProgress());
}
function loop(now) {
raf = requestAnimationFrame(loop);
var dt = Math.min(0.05, (now - last) / 1000 || 0); last = now;
if (!opts.autoplay || !visible || document.hidden) return;
clock += dt;
render(autoProgress());
}
function onScroll() {
if (opts.autoplay || sraf) return;
sraf = requestAnimationFrame(function () { sraf = 0; if (!destroyed && !opts.autoplay) render(scrollProgress()); });
}
function resize() {
W = Math.max(1, box.clientWidth); H = Math.max(1, box.clientHeight);
svg.setAttribute("viewBox", "0 0 " + W + " " + H);
svg.setAttribute("preserveAspectRatio", "none");
draw();
}
function update(partial) {
opts = Object.assign({ autoplay: "1", sweep: 5, tile: 120, angle: 45, background: "#0c1e18", stripe: "#e6ff55", band: "#1a7a4c", edge: "#fbf8f1" }, opts, partial || {});
opts.autoplay = String(opts.autoplay) === "0" || opts.autoplay === false ? 0 : 1;
opts.sweep = Math.max(0.2, num(opts.sweep, 5)); opts.tile = Math.max(8, num(opts.tile, 120)); opts.angle = num(opts.angle, 45);
box.style.setProperty("--ef-bpp-bg", opts.background);
box.style.setProperty("--ef-bpp-a", opts.stripe);
box.style.setProperty("--ef-bpp-b", opts.band);
box.style.setProperty("--ef-bpp-c", opts.edge);
pat.setAttribute("patternTransform", "scale(" + (opts.tile / 60).toFixed(4) + ") rotate(" + opts.angle + ")");
if (!destroyed) draw();
}
update(o);
resize();
if (!reduce) {
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
if ("IntersectionObserver" in window) { io = new IntersectionObserver(function (e) { visible = e[0].isIntersecting; if (visible) onScroll(); }); io.observe(box); }
last = performance.now(); raf = requestAnimationFrame(loop);
}
if ("ResizeObserver" in window) { ro = new ResizeObserver(function () { if (!destroyed) resize(); }); ro.observe(box); }
return {
update: update,
destroy: function () {
destroyed = true; cancelAnimationFrame(raf); cancelAnimationFrame(sraf);
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
if (io) io.disconnect();
if (ro) ro.disconnect();
}
};
}
// Usage
var fx = mount(document.querySelector("#my-root"), {"autoplay":1,"sweep":5,"tile":120,"angle":45,"background":"#0c1e18","stripe":"#e6ff55","band":"#1a7a4c","edge":"#fbf8f1"});How it works
One SVG pattern tile, 60 units square and turned 45 degrees, fills the whole box. Inside it a group holds wide main-colour squares, thin light wedges and second-colour bands, laid out past the tile's edges. Progress from 0 to 1 slides that group by 60 units left and 60 down in the tile's own turned space, with no easing, so the stripes glide along the diagonal and the tile edge clips them as they go; at the very end the tile is bare. With autoplay off, progress is tied to scroll the way a GSAP ScrollTrigger scrub is: 0 when the top of the element appears at the bottom of the window, 1 when its bottom leaves at the top, read with getBoundingClientRect on a passive scroll listener. With autoplay on, progress runs from 0 to 1 and back in a straight line over the sweep time, as if the page were scrolled down and up again.
Accessibility and performance
The SVG is decorative and has a text label. With prefers-reduced-motion it shows one still frame and ignores scroll. Nothing flashes and nothing needs the keyboard.
Each frame changes one transform attribute on one group; the browser redraws the pattern from a single small tile. Autoplay pauses off screen or in a hidden tab, and scroll mode only works when the page scrolls, at most once a frame.
| Section | Animated SVG backgrounds |
|---|---|
| Techniques | 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 | Barber Pole Scroll-Triggered Animation 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 make it follow scroll?
Set Autoplay to Off. The stripes then move only when the page scrolls, from the moment the element comes up from the bottom of the window until it leaves at the top.
Why does the tile go plain at the end of the sweep?
That is how the original pattern is drawn: the stripes are laid out so that a full slide carries them out of the tile. Autoplay turns round at that point and slides them back in.
Does it need GSAP or ScrollTrigger?
No. A scrub is the element's position in the window turned into a number from 0 to 1, which takes three lines with getBoundingClientRect.
Can I change the stripe angle?
Yes, use Angle. The original is 45 degrees. At 0 the tile sits square to the box and the stripes become flat bands with slanted ends; -45 tilts them the other way.