Skip to content

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.

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

<PlAnimateAppear className="flex flex-col gap-2">
  {services.map((service) => (
    <PlCard key={service.name} title={service.name} />
  ))}
</PlAnimateAppear>;
dart
import 'package:plass_ui/plass_ui.dart';

PlAnimateAppear(
  spacing: 8,
  children: <Widget>[
    for (final Service service in services) PlCard(title: Text(service.name)),
  ],
);

Props

PropTypeDefaultDescription
staggernumber70How long after one child the next one starts, in milliseconds. This is the whole effect — everything else is what a single child does
durationStepsharednumber0Milliseconds 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
distancenumber | 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
fadebooleantrueFades each child in as it settles
reversebooleanfalseRuns the list from the last child to the first
durationsharednumber380How 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'1How 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
renderReactElement | (props, state) => ReactElementRenders something other than a <div>
PropTypeDefaultDescription
children * List<Widget>The things that appear, one after another. The stagger counts children, so one child holding eight things is one step
orientationsharedPlassOrientationPlassOrientation.verticalWhich way the set runs. What a className does on the React side: there is no stylesheet here to put a display on the container
spacingdouble0The gap between children, in logical pixels
staggerDurationDuration(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
fromsharedPlassSidePlassSide.bottomWhich edge each child drifts in from
distancedouble12How 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
fadebooltrueFades each child in as it settles
reverseboolfalseRuns the list from the last child to the first
durationsharedDurationDuration(milliseconds: 380)How 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?1How 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

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.

React

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.

React

Accessibility

  • Under prefers-reduced-motion the 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

ReactFlutterWhy
the animation is written onto the children's own className and styleeach child is wrappedThere 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 stylesorientation and spacingNo stylesheet, so the widget has to lay its children out.
distance as a CSS lengthdoubleLogical pixels.
renderFlutter has no polymorphic element.
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