PlAnimateAppear
A list of things settling into place one after another. The effect belongs to the set rather than to any one item, so a reader's eye is walked down the list in the order it should be read.
import { PlAnimateAppear } from 'plass-ui';
<PlAnimateAppear className="flex flex-col gap-2">
{services.map((service) => (
<PlCard key={service.name} title={service.name} />
))}
</PlAnimateAppear>;import 'package:plass_ui/plass_ui.dart';
PlAnimateAppear(
spacing: 8,
children: <Widget>[
for (final Service service in services) PlCard(title: Text(service.name)),
],
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| stagger | number | 70 | How long after one child the next one starts, in milliseconds. This is the whole effect — everything else is what a single child does |
| durationStepshared | number | 0 | Milliseconds added to each child's duration. Negative is allowed; floored at 0 |
| fromshared | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | Which edge each child drifts in from |
| distance | number | string | '0.75rem' | How far each child travels. Short on purpose: this is a settling, not an entrance from off screen, and a long travel over a list of eight turns the whole block into something moving |
| fade | boolean | true | Fades each child in as it settles |
| reverse | boolean | false | Runs the list from the last child to the first |
| durationshared | number | 380 | 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' | 1 | 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 |
| render | ReactElement | (props, state) => ReactElement | — | Renders something other than a <div> |
| Prop | Type | Default | Description |
|---|---|---|---|
| children * | List<Widget> | — | The things that appear, one after another. The stagger counts children, so one child holding eight things is one step |
| orientationshared | PlassOrientation | PlassOrientation.vertical | Which way the set runs. What a className does on the React side: there is no stylesheet here to put a display on the container |
| spacing | double | 0 | The gap between children, in logical pixels |
| stagger | Duration | Duration(milliseconds: 70) | How long after one child the next one starts, in milliseconds. This is the whole effect — everything else is what a single child does |
| fromshared | PlassSide | PlassSide.bottom | Which edge each child drifts in from |
| distance | double | 12 | How far each child travels. Short on purpose: this is a settling, not an entrance from off screen, and a long travel over a list of eight turns the whole block into something moving |
| fade | bool | true | Fades each child in as it settles |
| reverse | bool | false | Runs the list from the last child to the first |
| durationshared | Duration | Duration(milliseconds: 380) | 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? | 1 | 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 |
The animation is written onto the children themselves rather than onto wrappers around them. A row of <li>s stays a row of <li>s, a grid's cells stay its direct children, and nothing about the layout changes because the list is being animated — so the class and style each child already had are kept alongside the ones this adds. Only a bare string has no element to write onto, and that one is wrapped in a <span>.
Every native <div> attribute passes straight through, and render swaps the container for another one.
It lays its children out, which the React build does not have to: there is no stylesheet here to put a display: flex on the container, so orientation and spacing are what a className would have done. Anything more elaborate than a row or a column belongs inside one child — which also makes that whole arrangement one step of the stagger. distance is a double in logical pixels.
The ten shared settings — duration, delay, easing, repeat, alternate, paused, trigger, play, once, threshold — are the same on every PlAnimate* component. delay is what happens before the first step, so it is added once rather than to every child.
Examples
stagger
The whole effect. Everything else is what a single child does — a short drift and a fade — and the stagger is what turns that into a sequence.
It counts children, not leaves: eight children are eight steps, and one child holding eight things is one step. That is also how to opt part of a list out — group it.
from and reverse
from is the edge each child drifts in from, and reverse runs the list from the last child to the first. The distance is short on purpose: this is a settling, not an entrance from off screen, and a long travel over a list of eight turns the whole block into something moving. For one thing arriving from a long way off, use PlAnimateSlide.
Accessibility
- Under
prefers-reduced-motionthe animation is dropped entirely and the whole list is simply there. - Nothing is hidden from a screen reader at any point. The children are all in the document from the first frame — what is staggered is when each one is drawn, not when it exists.
- Keep the total short. Eight children at 70ms is half a second before the last one lands; at 300ms it is two and a half, and a reader is looking at an incomplete list for most of it.
- The stagger is decoration, not order. If the sequence matters, it has to be in the markup.
- When the platform has animations turned off (
MediaQuery.disableAnimations) the effect is dropped entirely and the whole list is simply there. - Nothing is hidden from a screen reader at any point. Every child is in the tree from the first frame — what is staggered is when each one is drawn, not when it exists.
- Keep the total short. Eight children at 70ms is half a second before the last one lands; at 300ms it is two and a half.
- The stagger is decoration, not order. If the sequence matters, it has to be in the tree.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
the animation is written onto the children's own className and style | each child is wrapped | There is no class list to add to. Wrapping is transparent to a Flex, but a child that has to be the direct child of something — an Expanded — belongs outside this widget. |
the container is a bare <div> the caller styles | orientation and spacing | No stylesheet, so the widget has to lay its children out. |
distance as a CSS length | double | Logical pixels. |
render | — | Flutter has no polymorphic element. |
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. |