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.
import { PlAnimateMarquee } from 'plass-ui';
<PlAnimateMarquee gap="1.5rem" speed={45}>
{names.map((name) => (
<PlChip key={name}>{name}</PlChip>
))}
</PlAnimateMarquee>;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
| Prop | Type | Default | Description |
|---|---|---|---|
| orientationshared | 'horizontal' | 'vertical' | 'horizontal' | Which way the strip runs. Vertical needs a height on the box |
| reverse | boolean | false | Runs it the other way — left to right, or bottom to top |
| speed | number | 60 | How 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 |
| gap | number | string | '2rem' | The gap between items, and between the last item and the first of the next pass |
| copies | number | 2 | How 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 |
| pauseOnHover | boolean | true | Stops while the pointer is on it, so something scrolling past can actually be read or clicked |
| durationshared | number | measured from speed | How long one run takes, in milliseconds. A number, never a CSS string |
| delayshared | number | 0 | How long before it starts, in milliseconds |
| easingshared | string | the house curve | The easing curve, written the way CSS writes it |
| repeatshared | number | 'infinite' | 'infinite' | How many times it runs. 'infinite' rather than Infinity, because that word is what reaches CSS |
| alternateshared | boolean | false | Runs every other pass backwards, so a repeat returns instead of jumping |
| pausedshared | boolean | false | Holds 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 |
| playshared | boolean | — | Runs it when trigger is manual. Each false → true starts it over |
| onceshared | boolean | true | With trigger="visible", whether it runs only the first time. Off, it runs again every time the element comes back into view |
| thresholdshared | number | 0.2 | With trigger="visible", how much of the element has to be on screen before it counts as visible, from 0 to 1 |
| Prop | Type | Default | Description |
|---|---|---|---|
| children * | List<Widget> | — | The things that scroll past |
| orientationshared | PlassOrientation | PlassOrientation.horizontal | Which way the strip runs. Vertical needs a height on the box |
| reverse | bool | false | Runs it the other way — left to right, or bottom to top |
| speed | double | 60 | How 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 |
| gap | double | 32 | The gap between items, and between the last item and the first of the next pass |
| copies | int | 2 | How 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 |
| pauseOnHover | bool | true | Stops while the pointer is on it, so something scrolling past can actually be read or clicked |
| durationshared | Duration? | measured from speed | How long one run takes, in milliseconds. A number, never a CSS string |
| delayshared | Duration | Duration.zero | How long before it starts, in milliseconds |
| curveshared | Curve? | the house curve | The easing curve, written the way CSS writes it |
| repeatshared | int? | null | How 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 |
| alternateshared | bool | false | Runs every other pass backwards, so a repeat returns instead of jumping |
| pausedshared | bool | false | Holds the animation where it is |
| triggershared | PlassAnimateTrigger | PlassAnimateTrigger.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 |
| playshared | bool | false | Runs it when trigger is manual. Each false → true starts it over |
| onceshared | bool | true | With trigger="visible", whether it runs only the first time. Off, it runs again every time the element comes back into view |
| thresholdshared | double | 0.2 | With 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.
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.
Accessibility
- Under
prefers-reduced-motionthe 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. pauseOnHoveris 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. pauseOnHoveris 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
| React | Flutter | Why |
|---|---|---|
aria-hidden on the copies after the first | ExcludeSemantics | The framework's own name for the same exclusion. |
overflow: hidden on the box | UnconstrainedBox with clipBehavior: Clip.hardEdge | The 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 measured | the strip is measured and moved by that many pixels | A 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 length | double | Logical pixels. |
a reduced-motion animation: none | t held at 0 | The 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 milliseconds | Duration | The framework already has the type. |
easing as a CSS string | curve, a Curve | Dart's own name for the same thing. |
repeat: number | 'infinite' | int?, null never stops | There is no 'infinite' to write, and -1 would be a sentinel a caller has to look up. |
trigger="visible" via IntersectionObserver | watches the nearest Scrollable | There is no observer here; with no scrollable above it there is nothing to watch, so it runs. |
prefers-reduced-motion | MediaQuery.disableAnimations | The platform's own signal. |
className, style | — | There is no class list and no style attribute to pass through. |