DisplayReact
Content that scrolls on its own, in a loop, with a button that stops it.
Rendered by React 19.3.1
Loading React…Installation
npm i @hamur/reactNot on npm yet. The package goes out under the chosen name.
Anatomy
Import the component and assemble its parts:
<Marquee.Root>
<Marquee.PlayControl />
<Marquee.Viewport>
<Marquee.Content />
</Marquee.Viewport>
</Marquee.Root>Examples
Vertical
Headlines that scroll upwards in a Viewport 120 px high.
Reverse
A departures strip with a Reverse button that turns its direction; the content keeps its place.
Overflow
A track title with overflowOnly: still while it fits, scrolling once a longer title is shown.
Loop
An announcement with loopCount 3 that counts its passes and stops; the PlayControl runs it again.
API reference
13 props across 4 parts, in React syntax. className, style, render are listed once, under Shared props.
ContentMarquee.Content
The moving content, drawn once and then once more per copy needed to fill the Viewport; the copies are hidden from assistive technology and left out of the tab order. Renders a <div>.
| Data attribute | Description |
|---|---|
data-orientation | Indicates the motion axis. |
data-playing | Present while the motion is on. |
data-paused | Present while the motion is on but held by hover or a touch press. |
data-reverse | Present when the content moves toward the inline end (or the block end). |
data-overflowing | Present while the content does not fit in the Viewport. |
data-completed | Present once a finite loopCount has run. |
data-reduced-motion | Present while the user asks for reduced motion. |
data-index | The position of the Content: 0 for the original, 1 to n for the copies. |
data-copy | Present on each copy of the content. |
PlayControlMarquee.PlayControl
Starts and stops the motion; named by what pressing it does. Place it before the Viewport. Renders a <button>.
| Prop | Type | Default |
|---|---|---|
nativeButtonWhether the component renders a native <button> element when replacing it | boolean | true |
| Data attribute | Description |
|---|---|
data-orientation | Indicates the motion axis. |
data-playing | Present while the motion is on. |
data-paused | Present while the motion is on but held by hover or a touch press. |
data-reverse | Present when the content moves toward the inline end (or the block end). |
data-overflowing | Present while the content does not fit in the Viewport. |
data-completed | Present once a finite loopCount has run. |
data-reduced-motion | Present while the user asks for reduced motion. |
RootMarquee.Root
Groups all parts of the marquee: content that scrolls on its own, in a loop, with a control that stops it. Renders a <div>.
| Prop | Type | Default |
|---|---|---|
playingWhether the motion is on. | boolean | — |
defaultPlayingWhether the motion is on when the component is initially rendered. | boolean | true |
onPlayingChangeEvent handler called when the motion starts or stops. | ((playing: boolean, eventDetails: MarqueeRootPlayingChangeEventDetails) => void) | — |
orientationThe motion axis. | MarqueeOrientation | 'horizontal' |
reverseWhether the content moves toward the inline end (or the block end) instead of the start. | boolean | false |
speedThe speed of the motion, in CSS pixels per second. | number | 50 |
loopCountThe passes before the motion ends; Infinity loops forever and 0 stays still. | number | Infinity |
onLoopCompleteEvent handler called after each pass, with the number of passes run. | ((count: number) => void) | — |
onCompleteEvent handler called once when a finite loopCount has run. | (() => void) | — |
overflowOnlyWhether the content moves only while it does not fit in the Viewport. | boolean | false |
labelsThe PlayControl's names, in English by default. | Partial<MarqueeLabels> | — |
actionsRefA ref to imperative actions: play(), pause() and restart(). | React.RefObject<MarqueeRootActions | null> | — |
| Data attribute | Description |
|---|---|
data-orientation | Indicates the motion axis. |
data-playing | Present while the motion is on. |
data-paused | Present while the motion is on but held by hover or a touch press. |
data-reverse | Present when the content moves toward the inline end (or the block end). |
data-overflowing | Present while the content does not fit in the Viewport. |
data-completed | Present once a finite loopCount has run. |
data-reduced-motion | Present while the user asks for reduced motion. |
ViewportMarquee.Viewport
The box that clips the moving content on its axis. Its CSS gap is the space between copies. Renders a <div>.
| Data attribute | Description |
|---|---|
data-orientation | Indicates the motion axis. |
data-playing | Present while the motion is on. |
data-paused | Present while the motion is on but held by hover or a touch press. |
data-reverse | Present when the content moves toward the inline end (or the block end). |
data-overflowing | Present while the content does not fit in the Viewport. |
data-completed | Present once a finite loopCount has run. |
data-reduced-motion | Present while the user asks for reduced motion. |
| CSS variable | Description |
|---|---|
--marquee-duration | The duration of one pass, in seconds; 0s before the first measurement. |
--marquee-gap | The Viewport's gap on the motion axis, in pixels. |
--marquee-translate | The end of the keyframe for the axis and the direction. |
Shared props
Every part that renders an element takes these.
| Prop | Type |
|---|---|
classNameCSS class applied to the element, or a function that
returns a class based on the component's state. | string | ((state: State) => string | undefined) |
styleStyle applied to the element, or a function that
returns a style object based on the component's state. | React.CSSProperties | ((state: State) => React.CSSProperties | undefined) |
renderAllows you to replace the component's HTML element
with a different tag, or compose it with another component. | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State> |
Keyboard and accessibility
Keyboard
| Keys | Does | Part |
|---|---|---|
| EnterSpace | Stops or starts the motion; focus stays on the button and its name turns | PlayControl |
| Tab | Keyboard focus inside stops the motion; leaving starts nothing | Root |
| Tab | Walks the original's links once and leaves; the copies are not Tab stops | Content |
| Tab | Brings the focused item fully inside the Viewport | Viewport |
Accessibility
| Part | Sets | So that |
|---|---|---|
| Root | role="marquee" and aria-live="off", named by the app's aria-labelledby or aria-label; the app's role replaces it | the strip is said as content that changes on its own, and its changes are never read out |
| PlayControl | a button of type button named "Pause scrolling" while it moves and "Start scrolling" otherwise, unless it has text or an aria-label; no aria-pressed | the name says what a press does, and its change is the feedback for the press |
| PlayControl | aria-disabled="true" while overflowOnly holds the motion off because the content fits; it stays focusable | focus is not lost when the button stops applying |
| Viewport | a div with no role, clipped on its axis with overflow: clip, so focus never scrolls it | the moving box is not a scroll container a reader or the keyboard can get lost in |
| Content | the original with no role and no ARIA, data-index="0"; each copy aria-hidden="true", data-copy, and tabindex="-1" on every tabbable element inside it | the content is read and tabbed through once, while every copy still takes a click |
Focus order
- The
PlayControlcomes before theViewport, so it is reached before the moving content. - The original's focusable items follow, once each; the copies take no focus from Tab.
- Focus stays on the
PlayControlthrough the press and the change of its name. Nothing takes focus on mount, after a pass or at the end.