Skip to content

PlAnimateFade

Content arriving or leaving on opacity alone. Nothing moves, so nothing reflows and nothing is resampled, the one entrance that is safe on a block of text at any size.

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

<PlAnimateFade>
  <p>Two services restarted, no errors.</p>
</PlAnimateFade>;

<PlAnimateFade trigger="visible" duration={600}>
  <PlCard title="Usage">…</PlCard>
</PlAnimateFade>;
dart
import 'package:plass_ui/plass_ui.dart';

const PlAnimateFade(child: Text('Two services restarted, no errors.'));

const PlAnimateFade(
  trigger: PlassAnimateTrigger.visible,
  duration: Duration(milliseconds: 600),
  child: PlCard(title: Text('Usage'), child: Text('…')),
);

Props

PropTypeDefaultDescription
mode'in' | 'out''in'Whether the content arrives or leaves. out is the same keyframe run backwards, and it is held where it ends
fromnumber0The opacity it starts from, between 0 and 1. Raise it for content that should never be completely gone
durationsharednumber300How 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
timelineshared'auto' | 'view''auto'What advances the animation: the clock, or the reader's scroll position. view ignores duration, delay, repeat and trigger, and runs against range instead
rangesharedstring'entry 0% cover 45%'As CSS writes an animation-range. Only read when timeline is view
staggersharednumber0Milliseconds added to each child's delay. 0 plays the box itself; anything else moves the effect onto the children and takes it off the box
durationStepsharednumber0Milliseconds added to each child's duration. Negative is allowed; floored at 0
reversesharedbooleanfalseRuns the set from the last child to the first. Only the order turns round; each child still plays forwards
renderReactElement | (props, state) => ReactElementRenders something other than a <div>
PropTypeDefaultDescription
modePlassAnimateModePlassAnimateMode.enterWhether the content arrives or leaves. exit is the same curve run backwards and is held where it ends. enter/exit rather than in/out, because in is a reserved word in Dart
fromdouble0The opacity it starts from, between 0 and 1. Raise it for content that should never be completely gone
durationsharedDurationDuration(milliseconds: 300)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
child * WidgetWhat fades

Every native <div> attribute passes straight through, and render swaps the element for another one. A <section>, an <li>, whatever the surrounding markup needs.

duration and delay are Durations, and curve is a Curve. repeat is an int? where null never stops. There is no 'infinite' to write.

The ten shared settings (duration, delay, easing, repeat, alternate, paused, trigger, play, once, threshold) are the same on every PlAnimate* component and mean the same thing on each. What the shared style axes mean across the library is in prop conventions.

Three more move the effect off the box and onto the things inside it, stagger, durationStep and reverse. See Telling the children apart below.

Examples

trigger

Four ways in, and they are what the shared settings exist for. mount needs nothing from the caller. visible waits for the element to be scrolled into view, paused on its own first frame while it waits, so it is not fully drawn and then blinked out as it arrives. hover starts on the pointer and on focus, or the effect would be unreachable without a mouse. manual never runs on its own, and each false to true on play starts it over.

React

mode

out is the same keyframe run backwards rather than a second animation, which is why it costs nothing, and why it is held at the end: a faded-out element stays faded out instead of snapping back into view when the run finishes.

React

delay

A delay per element is what turns a set of things into a sequence. For a list where every child takes the same effect one after another, PlAnimateAppear counts the steps for you.

React

timeline

timeline="view" hands the effect to the reader's scroll position instead of the clock. It stops being something that happens at a moment and becomes something that tracks where the element sits in the scrollport, so scrolling back up plays it backwards, and a reader who stops halfway leaves it halfway.

React

Four settings stop meaning anything and are ignored rather than half-working. duration, delay and repeat all belong to a clock, and so does trigger, since the scroll position is the trigger. range replaces duration: it is an animation-range exactly as CSS writes it, and the default entry 0% cover 45% finishes while the element is still arriving rather than when it reaches the middle of the screen.

A browser without animation-timeline falls back to one clock-driven run, so the content still arrives. Degraded is allowed; blank is not, which is the reason this is two declarations behind an @supports rather than anything measured in JavaScript.

It is on the same six effects stagger is on, and absent from the same four: animation-timeline is a property of the element the keyframe runs on, and a marquee's motion is on a duplicated track, a typewriter's is not a keyframe at all, and a lighting's is on a pseudo-element. An endless decoration also has nothing a scroll position could advance it to.

Not offered. animation-timeline is a CSS property with no counterpart here; a scroll-linked effect in Flutter is an AnimationController driven from a ScrollPosition, which is an application's own wiring rather than something a widget can take as a prop.

Telling the children apart

stagger moves the effect off the box and onto the things inside it, one after another. It is milliseconds added to each child's delay, and 0 (the default) plays the box, which is what an effect should go on doing when it wraps one thing.

The box stops animating entirely once it is on: eight children fading in under a box that is also fading in is the same content faded twice, and the second one is not free.

React

durationStep gives each child a longer run than the last, or (negative) a shorter one, floored at 0. reverse starts from the end of the set: the order turns round and nothing else, because an effect that runs backwards is mode="out".

The step is per child, so what you pass matters, five children are five steps, and one child holding five things is one step. That is also how to opt part of a set out: group it.

The animation is written onto the children themselves rather than onto wrappers, so a row of <li>s stays a row of <li>s and a grid's cells stay its direct children. The cost is that a child has to accept a className and a style; one that does not is a child that will not animate. A bare string has no element to write onto and is the one case that gets a <span>.

All six single-keyframe effects take these three. PlAnimateMarquee, PlAnimateHeadline, PlAnimateTyping and PlAnimateLighting do not: the first three already read their children, and the last keeps its motion on a pseudo-element. PlAnimateAppear takes the same three under the same names, plus a stagger that defaults to 70.

There is no PlAnimateStagger, on purpose. A stagger is a differential rather than an effect, and a wrapper would be a second way to spell something the effects can already say. It is the same rule that keeps a Pulse (blink + alternate) and a Bounce (grow + alternate) out of the library.

A staggered set is PlAnimateAppear. The React build can write an effect onto arbitrary children because the caller's own CSS is still laying them out; here there is no stylesheet to do that, so a staggered effect has to own the row or the column too, which is what PlAnimateAppear is, and six more of it would be six more of it.

Accessibility

  • Under prefers-reduced-motion the animation is dropped entirely and the content is simply there. That is the opposite of what the loading indicators do, and the difference is what each of them is saying: a spinner that stops is lying about whether anything is happening, while an entrance that never plays has still delivered everything it was carrying.
  • The wrapper adds no role and no label. It is a <div> around content that already says what it is.
  • Nothing here is a way to hide content. A mode="out" element is still in the document and still read out; if it should be gone, unmount it.
  • trigger="hover" also starts on focus, so an effect on something keyboard-reachable runs for a reader who is not holding a mouse.
  • timeline="view" is dropped under prefers-reduced-motion for the same reason and with the same result, and it falls back to one clock-driven run in a browser that has no animation-timeline. Neither leaves anything blank.
  • When the platform has animations turned off (MediaQuery.disableAnimations) the effect is dropped entirely and the content is simply there. That is the opposite of what the loading indicators do, and the difference is what each of them is saying: a spinner that stops is lying about whether anything is happening, while an entrance that never played has still delivered everything it was carrying.
  • The widget adds no semantics of its own. It is an Opacity around content that already says what it is.
  • Nothing here is a way to hide content. A PlassAnimateMode.exit widget is still in the tree and still in the semantics; if it should be gone, take it out.
  • PlassAnimateTrigger.hover also starts on focus, so an effect on something keyboard-reachable runs for a reader who is not holding a mouse.

Differences from the React build

ReactFlutterWhy
duration, delay in millisecondsDurationThe framework already has the type, and a package taking int milliseconds would be the odd one out in every file that used it.
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. The same trade PlProgressLinear makes with a null value.
mode="in" | "out"PlassAnimateMode.enter / .exitin is a reserved word in Dart and cannot be an enum value.
trigger="visible" via IntersectionObserverwatches the nearest ScrollableThere is no observer here. With no scrollable above it there is nothing to watch, so it runs. Exactly as the React build does when the browser has none.
prefers-reduced-motionMediaQuery.disableAnimationsThe platform's own signal.
renderFlutter has no polymorphic element.
stagger, durationStep, reverseThe React build writes the effect onto the children themselves, so the caller's own layout is untouched. Flutter has no stylesheet to lay a set out with, so a staggered effect would have to own the row or the column as well, which is what PlAnimateAppear is, and six more of it would be six more of it.
timeline="view"animation-timeline is a CSS property with no counterpart here. A scroll-linked effect in Flutter is an AnimationController driven from a ScrollPosition, which is an application's own wiring rather than something a widget takes as a prop.
className, styleThere is no class list and no style attribute to pass through.

Released under the MIT License