Skip to content

PlAnimateMarquee

Content scrolling steadily past, forever. The content is laid down twice, so the moment the first copy has left, the second is standing precisely where it began, no seam, no jump, no empty frame.

React
tsx
import { PlAnimateMarquee } from 'plass-ui';

<PlAnimateMarquee gap="1.5rem" speed={45}>
  {names.map((name) => (
    <PlChip key={name}>{name}</PlChip>
  ))}
</PlAnimateMarquee>;
dart
import 'package:plass_ui/plass_ui.dart';

PlAnimateMarquee(
  gap: 24,
  speed: 45,
  children: <Widget>[for (final String name in names) PlChip(child: Text(name))],
);

Props

PropTypeDefaultDescription
orientationshared'horizontal' | 'vertical''horizontal'Which way the strip runs. Vertical needs a height on the box
reversebooleanfalseRuns it the other way — left to right, or bottom to top
speednumber60How fast the content travels, in pixels per second. A speed rather than a duration, so a strip of four logos and a strip of forty move at the same pace
gapnumber | string'2rem'The gap between items, and between the last item and the first of the next pass
copiesnumber2How many copies of the content are laid end to end. Two is enough for anything at least as wide as its container; raise it when the content is short enough to leave a hole behind itself
pauseOnHoverbooleantrueStops while the pointer is on it, so something scrolling past can actually be read or clicked
durationsharednumbermeasured from speedHow long one run takes, in milliseconds. A number, never a CSS string
delaysharednumber0How long before it starts, in milliseconds
easingsharedstringthe house curveThe easing curve, written the way CSS writes it
repeatsharednumber | 'infinite''infinite'How many times it runs. 'infinite' rather than Infinity, because that word is what reaches CSS
alternatesharedbooleanfalseRuns every other pass backwards, so a repeat returns instead of jumping
pausedsharedbooleanfalseHolds the animation where it is
triggershared'mount' | 'visible' | 'hover' | 'manual''mount'What starts it: mount as soon as it is on the page, visible when it is scrolled into view, hover while the pointer or focus is on it, manual only when play says so
playsharedbooleanRuns it when trigger is manual. Each false → true starts it over
oncesharedbooleantrueWith trigger="visible", whether it runs only the first time. Off, it runs again every time the element comes back into view
thresholdsharednumber0.2With trigger="visible", how much of the element has to be on screen before it counts as visible, from 0 to 1
PropTypeDefaultDescription
children * List<Widget>The things that scroll past
orientationsharedPlassOrientationPlassOrientation.horizontalWhich way the strip runs. Vertical needs a height on the box
reverseboolfalseRuns it the other way — left to right, or bottom to top
speeddouble60How fast the content travels, in pixels per second. A speed rather than a duration, so a strip of four logos and a strip of forty move at the same pace
gapdouble32The gap between items, and between the last item and the first of the next pass
copiesint2How many copies of the content are laid end to end. Two is enough for anything at least as wide as its container; raise it when the content is short enough to leave a hole behind itself
pauseOnHoverbooltrueStops while the pointer is on it, so something scrolling past can actually be read or clicked
durationsharedDuration?measured from speedHow long one run takes, in milliseconds. A number, never a CSS string
delaysharedDurationDuration.zeroHow long before it starts, in milliseconds
curvesharedCurve?the house curveThe easing curve, written the way CSS writes it
repeatsharedint?nullHow many times it runs. null is what never stops: there is no 'infinite' to write, and -1 would be a sentinel a caller has to look up
alternatesharedboolfalseRuns every other pass backwards, so a repeat returns instead of jumping
pausedsharedboolfalseHolds the animation where it is
triggersharedPlassAnimateTriggerPlassAnimateTrigger.mountWhat starts it: mount as soon as it is on the page, visible when it is scrolled into view, hover while the pointer or focus is on it, manual only when play says so
playsharedboolfalseRuns it when trigger is manual. Each false → true starts it over
oncesharedbooltrueWith trigger="visible", whether it runs only the first time. Off, it runs again every time the element comes back into view
thresholdshareddouble0.2With trigger="visible", how much of the element has to be on screen before it counts as visible, from 0 to 1

Every native <div> attribute passes straight through. There is no render here: the component owns its own structure — a clipping box with the copies inside it — so there is nothing meaningful to swap the outer element for.

gap is a double in logical pixels, and so is speed — logical pixels per second. duration is a Duration?: leave it out and the strip is measured, which is what speed is for.

There is no mode, no from and no fade. A marquee is a loop, not an arrival.

The rest of the shared settings — delay, easing, repeat, alternate, paused, trigger, play, once, threshold — mean what they mean everywhere else. duration is the exception: leave it out and the strip is measured, which is what speed is for.

Examples

speed

A speed rather than a duration, so a strip of four logos and a strip of forty move at the same pace instead of the long one becoming a blur. It is pixels per second, and the strip is re-measured whenever it changes size. Setting duration overrides the measurement entirely.

React

orientation and reverse

Vertical needs a height on the box — there is nothing else to clip against. reverse runs it bottom to top, or left to right.

React

Accessibility

  • Under prefers-reduced-motion the strip stops dead and the content sits where it is. Everything on it is still in the document and still reachable — it is a row of things, not a slideshow.
  • Only the first copy is read out. The rest carry aria-hidden, or a screen reader would announce everything on the strip as many times as it was laid down.
  • pauseOnHover is on by default and it is not decoration: content moving past a pointer cannot be clicked reliably, and a link inside a marquee that never stops is a link nobody can follow. It does not pause on focus, so keyboard-reachable content on a strip is a reason to reach for a static list instead.
  • Nothing that has to be read belongs here. A reader gets one pass at whatever speed you chose, and there is no way back.
  • When the platform has animations turned off (MediaQuery.disableAnimations) the strip stands where it started. Everything on it is still in the tree and still reachable — it is a row of things, not a slideshow.
  • Only the first copy is read out. The rest are behind ExcludeSemantics, or a screen reader would announce everything on the strip as many times as it was laid down.
  • pauseOnHover is on by default and it is not decoration: content moving past a pointer cannot be pressed reliably. It does not pause on focus, so focusable content on a strip is a reason to reach for a static list instead.
  • Nothing that has to be read belongs here. A reader gets one pass at whatever speed you chose, and there is no way back.

Differences from the React build

ReactFlutterWhy
aria-hidden on the copies after the firstExcludeSemanticsThe framework's own name for the same exclusion.
overflow: hidden on the boxUnconstrainedBox with clipBehavior: Clip.hardEdgeThe strip is longer than its box by design, so it has to be laid out against an unbounded main axis. A clip alone would clip the paint and leave the flex asserting that it overflowed.
a translate of -100% - gap, so nothing is measuredthe strip is measured and moved by that many pixelsA percentage translate resolves against the element's own box in CSS; here the measurement decides both the distance and the duration, and it is taken again whenever the strip changes size.
gap as a CSS lengthdoubleLogical pixels.
a reduced-motion animation: nonet held at 0The same outcome said two ways: a marquee's finished state is the content standing where it started, which is the opposite of what an entrance's is.
duration, delay in millisecondsDurationThe framework already has the type.
easing as a CSS stringcurve, a CurveDart's own name for the same thing.
repeat: number | 'infinite'int?, null never stopsThere is no 'infinite' to write, and -1 would be a sentinel a caller has to look up.
trigger="visible" via IntersectionObserverwatches the nearest ScrollableThere is no observer here; with no scrollable above it there is nothing to watch, so it runs.
prefers-reduced-motionMediaQuery.disableAnimationsThe platform's own signal.
className, styleThere is no class list and no style attribute to pass through.

Released under the MIT License