Skip to content

PlAnimateReveal

Content uncovered behind a moving edge. The only entrance in the set where nothing moves and no colour changes. Every pixel it has drawn is already where it will finally be.

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

<PlAnimateReveal render={<h2 />}>Everything is where it was.</PlAnimateReveal>;

<PlAnimateReveal from="top" trigger="visible" duration={700}>
  <PlDivider />
</PlAnimateReveal>;
dart
import 'package:plass_ui/plass_ui.dart';

const PlAnimateReveal(child: PlTypography('Everything is where it was.', level: PlTypographyLevel.h3));

const PlAnimateReveal(
  from: PlassSide.top,
  trigger: PlassAnimateTrigger.visible,
  duration: Duration(milliseconds: 700),
  child: PlDivider(),
);

Props

PropTypeDefaultDescription
mode'in' | 'out''in'Whether the content is uncovered or covered again. out is the same wipe run backwards, so it closes from the edge it opened towards
fromshared'top' | 'right' | 'bottom' | 'left''left'Which edge the wipe starts at. Physical, as it is everywhere in the library: a heading uncovered from the top is uncovered from the top in every writing direction
fadebooleanfalseFades in behind the wipe. Off by default, the opposite of every other effect, because a reveal not being a fade is the whole point of it
durationsharednumber520How 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 is uncovered or covered again. exit is the same wipe run backwards, so it closes from the edge it opened towards. enter/exit rather than in/out, because in is a reserved word in Dart
fromsharedPlassSidePlassSide.leftWhich edge the wipe starts at. Physical, as it is everywhere in the library: a heading uncovered from the top is uncovered from the top in every writing direction
fadeboolfalseFades in behind the wipe. Off by default, the opposite of every other effect, because a reveal not being a fade is the whole point of it
durationsharedDurationDuration(milliseconds: 520)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 is uncovered

Every native <div> attribute passes straight through, and render swaps the element for another one, which is worth using here more than anywhere else in the set, because a reveal is usually wrapped around a heading or a rule that is already the right element.

The clip is applied while painting, so the widget is laid out once at its full size and nothing beside it is laid out again. That is the difference from an Align with a widthFactor, which would resize the box and push its neighbours around.

from is physical (top, right, bottom, left) as PlassSide is everywhere in the library. A heading uncovered from the top is uncovered from the top in every writing direction.

The ten shared settings (duration, delay, easing, repeat, alternate, paused, trigger, play, once, threshold) are the same on every PlAnimate* component. The four trigger values are shown on the PlAnimateFade page. timeline="view" and range are there too, and hand the effect to the reader's scroll position instead of the clock.

Three more move the effect off the box and onto the things inside it: stagger holds each child back by its position, durationStep gives each one a longer or shorter run than the last, and reverse starts from the end of the set. They are on all six single-keyframe effects and are shown on the PlAnimateFade page.

Choosing an entrance

The set has five other ways of arriving, and each of them changes something about the element while it does it. This one changes how much of the element is drawn and nothing else.

  • PlAnimateFade changes the ink. Safe on any block of text, and the first one to use, but a faded heading is a heading somebody has to read twice.
  • PlAnimateSlide changes the position. What it says is "this has arrived from somewhere", which is a lie about a rule that has always belonged between those two sections.
  • PlAnimateGrow and PlAnimateZoom change the size, so the text inside is resampled at every frame.
  • PlAnimateReveal changes neither. Use it where the position is the information: a heading over the paragraph it belongs to, a divider between two sections, the plot area of a chart, a column of figures that must not be read from the wrong place.

It is also the cheapest of the five to lay out, because there is nothing to lay out. No wrapper, no overflow box, no second element in the flow. The clip paints less of the element and the page around it never learns that anything happened.

Examples

from

Four edges, and mode="out" closes from whichever one it opened towards.

React

fade

Off by default, which is the opposite of every other effect that offers it. Turning it on asks for two entrances at once, and the reason to have reached for this one is usually that the first was the problem.

Accessibility

  • Under prefers-reduced-motion the animation is dropped entirely and the content is simply there, including the clip, so nothing is left half drawn.
  • Nothing reflows while it runs, and nothing is resampled. That makes it as safe on a block of text as a fade, and safer than anything that scales.
  • The clipped part of the element is still in the document and still read out. This is an entrance, not a way to hide something: if it should be gone, unmount it.
  • A caller's own clip-path on the same element is overwritten while the effect runs. Put one of them on a wrapper.
  • When the platform has animations turned off (MediaQuery.disableAnimations) the effect is dropped entirely and the content is simply there.
  • Nothing is laid out again while it runs. The clip happens at paint time, so neither the widget nor anything beside it changes size.
  • The clipped part of the widget is still in the tree and still in the semantics. This is an entrance, not a way to hide something.

Differences from the React build

ReactFlutterWhy
clip-path: inset()ClipRect with a clipperThe same rectangle, named the way each platform names it.
mode="in" | "out"PlassAnimateMode.enter / .exitin is a reserved word in Dart.
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.
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