Skip to content

PlAnimateTyping

Text appearing one character at a time. The whole string is in the document from the first frame, so the effect costs a reader who cannot see it nothing and reflows nothing for a reader who can.

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

<PlAnimateTyping text="npm install plass-ui" speed={14} hold={1600} erase repeat="infinite" />;
dart
import 'package:plass_ui/plass_ui.dart';

const PlAnimateTyping(
  'flutter pub add plass_ui',
  speed: 14,
  hold: Duration(milliseconds: 1600),
  erase: true,
  repeat: null,
);

Props

PropTypeDefaultDescription
textstringThe text, when it is easier to pass than to nest. Overrides children
speednumber24How fast it is typed, in characters per second, so a long paragraph and a short one are typed at the same pace rather than in the same time
holdnumber1400How long the finished text is held before it repeats, in milliseconds
erasebooleanfalseDeletes the text again before repeating, rather than clearing it in one frame. Only means anything when repeat is more than once
eraseSpeednumbertwice speedHow fast it is deleted, in characters per second. Deleting is usually about twice as fast as typing, which is what a person actually does
caretbooleantrueThe block after the text
caretCharReactNode'|'What the caret is drawn as
durationsharednumberspeed decidesHow long one run takes, in milliseconds. A number, never a CSS string
delaysharednumber0How long before it starts, in milliseconds
repeatsharednumber | 'infinite'1How many times it runs. 'infinite' rather than Infinity, because that word is what reaches CSS
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
PropTypeDefaultDescription
text * StringThe text to type. A String and not a widget: a typewriter reveals a string one grapheme at a time, and there is no honest way to reveal half of a link
speeddouble24How fast it is typed, in characters per second, so a long paragraph and a short one are typed at the same pace rather than in the same time
holdDurationDuration(milliseconds: 1400)How long the finished text is held before it repeats, in milliseconds
eraseboolfalseDeletes the text again before repeating, rather than clearing it in one frame. Only means anything when repeat is more than once
eraseSpeeddouble?twice speedHow fast it is deleted, in characters per second. Deleting is usually about twice as fast as typing, which is what a person actually does
caretbooltrueThe block after the text
caretCharString'|'What the caret is drawn as
durationsharedDuration?speed decidesHow long one run takes, in milliseconds. A number, never a CSS string
delaysharedDurationDuration.zeroHow long before it starts, in milliseconds
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
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

Every native <div> attribute passes straight through, except children, which is the text. There is no render, no easing and no alternate: the component owns its two spans, and a typewriter advances a character at a time rather than along a curve.

The text is the first positional argument and a plain String, the way PlTypography takes its data. There is nothing to flatten here: a typewriter reveals a string one grapheme at a time, so its input is a string. It draws in whatever DefaultTextStyle it is sitting in.

Only text is typed. Pass a string, or strings; an element among the children contributes its text and nothing about its markup, because there is no honest way to reveal half of a link.

duration is honoured as the time for the whole string, and it overrides speed. speed is the natural unit here — a long paragraph and a short one should be typed at the same pace, not in the same time — so it is the default.

Examples

speed

Characters per second. Around 24 reads as somebody typing; below 10 is a machine printing, and above 60 is closer to the line simply appearing.

React

erase

repeat, hold and erase are what make it a loop: type, hold, delete, type again. Without erase a repeat clears in one frame, which is right for a line being replaced rather than rewritten.

React

Accessibility

  • The whole string sits in a clipped box that a screen reader reads once, and the visible copy that animates is aria-hidden. Nobody is made to sit through the performance.
  • Under prefers-reduced-motion the text is simply there. Not "nothing happens" — that is the only outcome that still delivers what the component was carrying.
  • The advance is by grapheme, not by code point. 👩‍👩‍👧 is one character to a reader and seven code points to JavaScript, and a typewriter that advanced by code points would spend four frames assembling it out of parts that mean nothing on their own.
  • The box is not laid out from the characters that have arrived, so the text around it does not reflow on every frame. It will, however, be as wide as its container allows — give a one-line effect a white-space: nowrap or a width if the wrap matters.
  • The whole string is the widget's accessible label and the drawn copy is behind ExcludeSemantics, so a screen reader is given the text once and is not made to sit through the performance.
  • When the platform has animations turned off (MediaQuery.disableAnimations) the text is simply there. Not "nothing happens" — that is the only outcome that still delivers what the widget was carrying.
  • The advance is by grapheme, not by code point. 👩‍👩‍👧 is one character to a reader and seven code points to Dart.
  • The box the whole string will need is held from the first frame, so nothing around it is laid out again as the characters arrive.

Differences from the React build

ReactFlutterWhy
text or children, flattened to a stringone positional StringThere is nothing to flatten: a typewriter's input is text, so it takes text.
a clipped copy for a screen reader plus an aria-hidden visible oneSemantics(label:) over an ExcludeSemanticsSame two jobs, one node fewer.
the box is not laid out from the arrived charactersthe full string is drawn invisibly under the partial oneFlutter lays a Text out from what it holds, so the space has to be reserved by something that holds the whole string.
Intl.SegmenterString.charactersBoth know where a grapheme ends; this one ships with the framework.
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