A logo wall that fills itself.
Give it however many items you have. The component measures one pass of your content and
clones it until the track covers twice the container, so the loop never shows a gap — six
logos or sixty, same markup. Clones are marked aria-hidden and
inert, and their ids are stripped, so the duplication stays
invisible to screen readers and to getElementById.
Pixels per second, either way.
speed is plain px/sec — not a duration, so adding items never changes how fast
it moves. direction flips it. Both are live: set the property or the attribute
and the next frame picks it up, mid-scroll, without a restart.
Set the speed from a media query.
This is why v2 dropped mobile-speed / desktop-speed /
breakpoint. Instead of one hard-coded breakpoint baked into the component, the
speed reads from the --scrolling-content-speed custom property — so it obeys
whatever breakpoints your design system already has, media queries and container queries
alike. Resize this window and watch the readout change.
Hover to pause. Drag to scrub.
Both are on by default and both are one attribute away from off. Dragging uses pointer
capture, so the gesture survives leaving the element and there are no window-level
listeners to leak. On touch, touch-action: pan-y lets the browser arbitrate:
vertical swipes scroll the page, horizontal ones scrub the track — no axis-detection
heuristic in JS.
Stack them, oppose them.
Each instance owns its own loop and its own measurements, so rows at different speeds and directions stay independent. Nothing is shared and nothing is global — drop as many on a page as the design wants.
Edges that dissolve.
overflow: hidden cuts items off with a hard vertical edge, which reads as a
box with content sliding behind it. Add fade and the boundaries become a mask
instead, so items thin out into the background — the thing that actually sells "endless".
Bare fade uses a 4rem ramp; fade="8rem" or the
--scrolling-content-fade property sets your own. Every marquee on this page
uses it.
The unglamorous half.
Most of v2 is the boring correctness a marquee needs to survive a real page. Content that
measures late — images, webfonts, an ancestor that starts hidden — is picked up by a
ResizeObserver instead of a hopeful setTimeout, so the clone
count is derived from real widths every time. The per-frame delta is clamped, so returning
to a backgrounded tab resumes instead of teleporting. And
prefers-reduced-motion stops the loop outright while leaving drag intact — the
marquee becomes a scrubbable strip rather than perpetual motion.
Write the markup. Then change it.
Author the <scrolling-item> yourself and the component moves nothing — it
measures your element, in place, and appends clones after it. That is what makes it safe
inside a framework that owns the DOM it rendered. Edit that item afterwards and a
MutationObserver rebuilds every clone on the next frame, so the copies never
go stale. Type below, or append a chip.
The whole API, on one screen.
Six attributes, four custom properties, three methods, four events. That's everything.
Attributes
speed60--scrolling-content-speed when that property is set.
directionleftleft · rightpausedstart() and stop() write.pause-on-hovertrue"false" to keep scrolling under the cursor.fadefade uses 4rem.
dragtrue"false" to disable scrubbing. Deliberately not named
draggable, which is a real global HTML attribute.
CSS Custom Properties
--scrolling-content-speedspeed attribute, so breakpoints can drive
it. Left unset by the stylesheet on purpose — a default there would always beat the
attribute.
--scrolling-content-fade4remfade is present.--scrolling-content-gap1rem--scrolling-content-item-padding0<scrolling-item>.Methods & Properties
start()paused.stop()paused.refresh()rebuild()[data-clone] and refill from the source item. Called
automatically when that item's content changes — reach for it only when you've
replaced the <scrolling-item> element itself.
speeddirection'left' · 'right'pausedEvents
scrolling-content:startscrolling-content:stopscrolling-content:drag-startscrolling-content:drag-endMigrating from v1
mobile-speed desktop-speed breakpointspeed + --scrolling-content-speed<scrolling-track gap="30">--scrolling-content-gap<scrolling-item pad="10">--scrolling-content-item-paddingstop() then hover outstop() stays stoppedpaused is now real state; hovering out no longer overrides an explicit
stop.
@layer scrolling-content stylesheet, so plain
author selectors override it — no !important needed.