Skip to content

PlAnimateShake

A refusal. The one effect in the set that is a response rather than an entrance, so it starts held still, and replay is how a caller says "again".

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

<PlAnimateShake replay={attempts}>
  <PlTextField label="Password" type="password" error={error} invalid />
</PlAnimateShake>;
dart
import 'package:plass_ui/plass_ui.dart';

PlAnimateShake(
  replay: attempts,
  child: PlTextField(label: const Text('Password'), error: error, invalid: true),
);

Props

PropTypeDefaultDescription
replayunknownPlays the shake again whenever this value changes, and never on the first render. A refusal can happen twice, and a boolean cannot say "again"
distancenumber | string6How far it travels either side of where it started
durationsharednumber400How 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
replayObject?Plays the shake again whenever this value changes, and never on the first render. A refusal can happen twice, and a boolean cannot say "again"
distancedouble6How far it travels either side of where it started
durationsharedDurationDuration(milliseconds: 400)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 refused

What the shared animation props mean is on any of the other transitions.

replay is why it exists

A refusal can happen twice, and play (being a boolean) cannot say "again". Replaying with it means toggling off and on: two renders for one event, and a piece of state whose only job is to be flipped back.

A value that has changed is the closest React has to an event, and the count of failed attempts a form already keeps is exactly that value.

tsx
const [attempts, setAttempts] = useState(0);

<PlAnimateShake replay={attempts}>…</PlAnimateShake>;

It never plays on the first render. A shake that shook itself on mount would be answering an event that has not happened.

A reaction to an event

Every other effect here answers "how does this content arrive" and starts on mount. This one answers something the reader did, so it starts held still (trigger defaults to manual), and plays only when it is told to.

It is also not in PlassAnimation, the union mode and stagger are built on, for PlAnimateFloat's reason: that union is the set of ways content can arrive, and a response is not an arrival.

The rest position

Three shudders either side of home and back to nothing.

That matters more here than anywhere else in the group, because this is the one effect a caller will run over content that is still being typed into. A field left a few pixels off its label would be a worse defect than the error it was reporting.

Examples

A locked control

tsx
<PlAnimateShake replay={refusals}>
  <PlButton disabled>Delete workspace</PlButton>
</PlAnimateShake>

A shorter, wider shudder

tsx
<PlAnimateShake replay={attempts} distance={10} duration={300}>

Accessibility

  • A reader who asked for less motion sees none of it, and that is exactly why the words matter more than the shake. Whatever the refusal is saying has to be said in text as well (an error on the field, a message in a live region), and the shake is emphasis, never the message.
  • Shaking a field does not tell a screen reader anything. Pair it with the field's own error and invalid, which do.
  • It is decoration around the content, not a wrapper that changes what the content is: what is inside keeps its own role, its own focus and its own name.

Released under the MIT License