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.
import { PlAnimateFade } from 'plass-ui';
<PlAnimateFade>
<p>Two services restarted, no errors.</p>
</PlAnimateFade>;
<PlAnimateFade trigger="visible" duration={600}>
<PlCard title="Usage">…</PlCard>
</PlAnimateFade>;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
| Prop | Type | Default | Description |
|---|---|---|---|
| mode | 'in' | 'out' | 'in' | Whether the content arrives or leaves. out is the same keyframe run backwards, and it is held where it ends |
| from | number | 0 | The opacity it starts from, between 0 and 1. Raise it for content that should never be completely gone |
| durationshared | number | 300 | 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 |
| 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 |
| rangeshared | string | 'entry 0% cover 45%' | As CSS writes an animation-range. Only read when timeline is view |
| staggershared | number | 0 | Milliseconds 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 |
| durationStepshared | number | 0 | Milliseconds added to each child's duration. Negative is allowed; floored at 0 |
| reverseshared | boolean | false | Runs the set from the last child to the first. Only the order turns round; each child still plays forwards |
| render | ReactElement | (props, state) => ReactElement | — | Renders something other than a <div> |
| Prop | Type | Default | Description |
|---|---|---|---|
| mode | PlassAnimateMode | PlassAnimateMode.enter | Whether 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 |
| from | double | 0 | The opacity it starts from, between 0 and 1. Raise it for content that should never be completely gone |
| durationshared | Duration | Duration(milliseconds: 300) | 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 |
| child * | Widget | — | What 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.
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.
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.
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.
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.
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-motionthe 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 underprefers-reduced-motionfor the same reason and with the same result, and it falls back to one clock-driven run in a browser that has noanimation-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
Opacityaround content that already says what it is. - Nothing here is a way to hide content. A
PlassAnimateMode.exitwidget is still in the tree and still in the semantics; if it should be gone, take it out. PlassAnimateTrigger.hoveralso 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
| React | Flutter | Why |
|---|---|---|
duration, delay in milliseconds | Duration | The 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 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. The same trade PlProgressLinear makes with a null value. |
mode="in" | "out" | PlassAnimateMode.enter / .exit | in is a reserved word in Dart and cannot be an enum value. |
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. Exactly as the React build does when the browser has none. |
prefers-reduced-motion | MediaQuery.disableAnimations | The platform's own signal. |
render | — | Flutter has no polymorphic element. |
stagger, durationStep, reverse | — | The 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, style | — | There is no class list and no style attribute to pass through. |