Marquee
Content that moves in a loop, a logo wall, a ticker, a column of live events.
A marquee is content that repeats without end: the customers on a logo wall, the rates on a ticker, the payments settling in a column beside a dashboard. It is the one component here whose purpose is motion, and it says so in its props: the axis to travel along, the direction, the speed, the distance between items, and whether the loop runs at all.
What makes it a component rather than a div with an animation is the seam. The children are
rendered twice and the track travels by half its own length, so the loop returns to content rather
than to empty space; the second copy is hidden from assistive technology, so the content is read
once.
Usage
import { Marquee } from "@asheeui/web";
<Marquee speed="slow" gap="3rem" pauseOnHover fadeEdges>
{customers.map((customer) => (
<Typography key={customer} role="heading-sm" as="span">
{customer}
</Typography>
))}
</Marquee>
children is an array, and it is the array that is repeated, so the list a page passes is the
list a reader sees, drawn twice rather than generated once. Each child needs its own key for the
same reason any list does.
axis is x, the registered value, or y for a column; a vertical marquee has no width to claim
and states h-full, so the frame it sits in supplies the height. direction is forward, the
registered one, which moves from right to left on the horizontal axis and from bottom to top on
the vertical, or reverse.
gap is a CSS length, registered as 1.5rem, and it is applied both between the items of one
copy and between the two copies, so the loop's seam is spaced like everything else.
Speed, and what the reader can read
speed is a preset, slow at 50 seconds a cycle, normal at 30, fast at 15, or a number of
seconds for one cycle, which is the same scale stated exactly. normal is the registered value.
Speed is the one prop worth thinking about in seconds rather than in tokens, because the duration
is fixed and the distance is not: a cycle covers one copy of the content, so the same preset moves
faster when the copy is wide, a wall of long customer names, and more gently when it is short. A
ticker of short strings reads at fast, and a logo wall reads at slow, because a reader has to
recognise a word they were not expecting.
pauseOnHover, registered false, stops the track while a pointer rests on it or while
anything inside it has keyboard focus, so a reader who cannot use a pointer can still stop the
loop, the class covers group-hover and group-focus-within, so it needs no state and no
rerender. A marquee whose content holds nothing focusable therefore has only the reduced-motion
opt-out below. fadeEdges, registered false, paints a gradient over each end so the content
dissolves into the background instead of being cut off at the frame's edge.
Motion a reader can decline
Two things stop the loop: the isAnimated prop, registered true, and the document's
prefers-reduced-motion preference. The component reads the preference through
usePrefersReducedMotion and renders the content once, at rest, when either says stop.
A stopped marquee is a different layout rather than a paused one: one copy is drawn in a frame
that scrolls along the axis and wraps on the cross axis, so nothing is clipped and nothing is
duplicated. That is also why the duplicate is inert while the track moves, a link inside a
moving marquee is one tab stop, announced once. The demo's own switch shows the difference between
the moving and the resting form.
Props
Every option is registered, so a product's marquee is stated once under components.marquee:
axis x, direction forward, speed normal, gap 1.5rem, pauseOnHover false,
fadeEdges false and isAnimated true. There is no colour, size or radius to resolve, because
a marquee draws no surface of its own, what it draws is the consumer's nodes, in a consumer's
frame.
children, itemClassName, className and style are instance-only. What moves and how it is
dressed in one place is a statement about that place; how fast and which way is a statement about
the product.
Accessibility
- The content is drawn twice for the seam, and the second copy is
aria-hidden, so a reader hears each item once rather than twice in a row. - The loop yields to
prefers-reduced-motion: reduceand toisAnimated, so a reader who has asked their system for less motion gets a still frame without the page having to know. pauseOnHoveris a pointer's gesture. There is no control in the page that stops the loop, so a keyboard or touch reader cannot pause it, and a screen that has to offer that states its own control overisAnimated, the demo in the previous section is the smallest version of it.aria-hiddenhides a subtree from assistive technology without removing it from the tab order, so a link or button inside a marquee is a tab stop in both copies. A marquee is for content that is read, not pressed.
Native
@asheeui/native renders a Marquee with the same props, driving an animated scroll view rather
than a stylesheet keyframe. It yields to the platform's own motion setting the way the web yields
to the media query, pauseOnHover has nothing to pause on because a touch screen has no pointer,
and fadeEdges is not painted: a gradient needs a drawing library the package does not depend on,
so the option resolves and changes nothing. gap is converted from a CSS length, rem at 16
units, px at one, and falls back to 24 when it names a unit the platform cannot state. See
React Native.