Marquee
A purely presentational component that loops long content seamlessly via a slot; supports pause on hover and static fallback under prefers-reduced-motion. Scroll speed is a true pixels-per-second value (the component measures content width to derive duration), and content shorter than the container is auto-filled; when max-loops finishes, oas-finish is emitted (detail { loops }).
Breaking change: speed semantics changed from "seconds" to "pixels per second"
In previous versions speed was the duration of a single loop (seconds): speed="8" meant one full loop in 8 seconds — the wider the content, the faster it visually moved, so the same value produced wildly different speeds across containers. Now speed is the true scroll speed (pixels per second, default 48): the component measures the content width with a ResizeObserver and derives the animation duration (duration = content width ÷ speed), so the visual speed stays constant regardless of content or container width.
Migration guide: small legacy values (e.g. speed="8") now mean 8px/s (extremely slow) — re-set them to a target speed (24–120 is common). To keep duration semantics, override the CSS variable --oas-marquee-duration directly (the measurement write will still overwrite it; not recommended).
Basic Usage
Default looping scroll (48px/s)
Speed Control
speed=96 fast / speed=24 slow (pixels per second)
With the same speed="48", a 200px short text and a 2000px long text scroll at exactly the same visual speed (under the old "duration" semantics they differed by 10x) — this is why the speed semantics changed.
Auto-fill
When content is shorter than the container, duplicating it only once leaves blank space on the right after half a loop (a broken seam). The component measures the container and content widths and auto-fills the clone group with enough copies, so the viewport always has content during a full one-group translation — seamless, no broken seam.
auto-fill: short content loops seamlessly in a wide container
- The clone count is capped (50): with extremely short content in an extremely wide container, blank space is preferred over unbounded DOM growth; for heavy content like image walls, keep the content itself at least one viewport wide.
- The fill count is re-derived automatically when the container size changes (ResizeObserver).
Vertical Scrolling
orientation="vertical" switches to vertical scrolling (announcement-style long text). The container must have a fixed height (overflow clipping only makes sense with one), given via host styles.
orientation=vertical: vertical scrolling (fixed 96px height container)
A vertical marquee is continuous-flow semantics; for "scroll one item at a time and pause", use oas-carousel with vertical direction.
Reverse Scrolling
reverse: opposite directions (two rows in counter-flow)
Multiple Rows
The component keeps a single continuous scrolling row; a multi-row marquee is composed from several oas-marquee elements interleaved with reverse (no extra attribute — stacking them vertically yields multiple rows):
Multiple rows: forward / reverse / forward
Edge Fading
fade-edges fades out both container edges (mask-image mask, off by default — fading softens edges and changes existing appearances, enable on demand). The fade width is controlled by the CSS variable --oas-marquee-fade-size (default 24px).
fade-edges: logo wall with faded edges
Pause on Hover
pause-on-hover: pauses on mouse hover / focus
Dynamic Content Updates
When the host appends to or modifies the slot, the component rebuilds the clone group while preserving the animation phase (it records the current phase and resumes with a negative animation-delay), so the scroll position users see does not jump. The instance below appends a live notice every 3 seconds:
Appending content: no scroll jump
Element Content
slot supports arbitrary element combinations
Image / Logo Wall
Content is not limited to text — putting images or logos in the slot produces the classic seamless "brand wall" scrolling.
Logo wall (seamless loop + faded edges)
Image scrolling (img)
Limited Loop Count (max-loops)
max-loops limits the number of loop rounds (positive integer): after playing N rounds the animation freezes on the final frame (animation-fill-mode: forwards) and oas-finish is dispatched once (detail: { loops }). When unset or set to an invalid/non-integer value the marquee loops infinitely. A "round" equals the shift distance (one copy of the content), independent of content width.
Under prefers-reduced-motion the animation is disabled and content is shown statically — nothing "finishes playing", so oas-finish is not dispatched.
max-loops=3: freezes after 3 rounds and dispatches oas-finish
Playing…
RTL (Right-to-Left)
When the container is in an RTL writing direction (host or ancestor dir="rtl", or oas-config-provider direction injection), the translation direction mirrors automatically: content flows to the right and new content enters from the left edge (mirroring LTR's "flow left, enter from right"); seamless looping and auto-fill are unaffected. reverse composes orthogonally with the mirror — under RTL, reverse flows to the left. Vertical scrolling is independent of writing direction.
dir=rtl: mirrored translation (content flows rightward)
API
oas-marquee
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
dir | Writing direction for RTL detection; horizontal translation mirrors under RTL | — | — |
fade-edges | Boolean; when present, fades out both container edges with a mask-image (off by default); fade width via --oas-marquee-fade-size | — | — |
max-loops | Limit loop rounds (positive integer): freezes on the final frame after N rounds and dispatches oas-finish once; missing/invalid = infinite | string | — |
orientation | Scroll direction: horizontal (default) / vertical (vertical scrolling; the container needs a fixed height) | — | — |
pause-on-hover | Boolean; when present, pauses the animation on hover/focus (animation-play-state: paused) | — | — |
reverse | Boolean; when present, scrolls in the opposite direction | boolean | — |
speed | Scroll speed in pixels per second (default 48); the animation duration is derived from the measured content width (duration = distance / speed); invalid or non-positive values fall back to the default | string | — |
Events
| Event | Description |
|---|---|
oas-finish | Dispatched once when the max-loops run finishes, detail: { loops } |
Slots
| Name | Description |
|---|---|
| default | Scrolling content |
CSS Variables
| CSS Variable | Default |
|---|---|
--oas-marquee-duration | 20s |
--oas-marquee-fade-size | 24px |
--oas-marquee-shift | 50% |
- When the system enables "reduce motion" (
prefers-reduced-motion: reduce), the animation is disabled and content is shown statically. - The duplicated content group carries
aria-hidden, so screen readers do not read it twice. - No other events; purely presentational, except a single
oas-finishwhenmax-loopsfinishes.