kinetic_text
Kinetic typography for Flutter â per-letter, per-word and per-line text motion from ONE shaped paragraph: staggered reveals, sheen and shimmer sweeps, gradient and rainbow ink on any span, sparkle, marker highlight, draw-on underline, typewriter, a text morph that rewrites a label in place, and a ticker whose digits turn like an odometer.
- ðŠķ Zero dependencies. Nothing beyond the Flutter SDK. No shaders to ship, no Rive, no Lottie.
- ðĪ Every script stays joined. The text is laid out once as a whole, and
each letter is re-drawn from that one layout. Arabic and Kurdish keep their
cursive joins, ligatures keep their shape, bidi keeps its order â a
per-letter effect never splits the string into separate
Texts. - ðŊ Effects compose. A cascade, a shimmer and a rainbow on the same word all write into one pose per letter and paint in one layer. Stagger in reading order, reverse, from the centre, from the edges, or scattered.
- âŋ Reduced motion is a first-class path. One-shots resolve to their end state, loops go quiet, fills and markers stay.
- ð Lays out like
Text. Reads the ambientDefaultTextStyle,Directionality, text scale and bold-text setting; wraps to its constraints; reports intrinsic sizes and a text baseline (so it sits in a baseline-alignedRowor anIntrinsicWidthlike any text); speaks its plain text to assistive technology. - ð§ Cheap when still. A finished reveal is one plain paint. Effects compare by value, so a parent rebuild never re-lays the paragraph out.
By Armanic Studio. Sibling of iconic_morph.
Install
dependencies:
kinetic_text: ^0.2.0
import 'package:kinetic_text/kinetic_text.dart';
Quick start
// A headline cascading in, word by word.
KineticText(
'Fresh harvest, every morning',
style: headline,
unit: TextUnit.word,
effects: const [Rise(distance: 10)],
);
// A rainbow on one word, a marker under another â the rest plain.
KineticText.rich([
const TextRun('Pay '),
TextRun('12,000', effects: [Highlight(color: tint), Underline(color: accent)]),
const TextRun(' before '),
TextRun('Friday', effects: [GradientInk.rainbow(seed: accent, flow: const Duration(seconds: 4))]),
], style: body);
// A button label that shimmers while a payment clears.
KineticText('Pay now', style: label, effects: const [Shimmer()]);
// A figure whose digits turn like an odometer â retarget it mid-roll and
// every wheel just redirects.
TickerText(formatted, style: figure);
// A value that rewrites itself â the changed letters roll on the same drum.
TextMorph('$amount', style: figure, morph: const MorphStyle.roll());
// A title that dissolves into the next one under a sheen.
TextMorph(title, style: headline, morph: MorphStyle.sheen(sheen: [accent]));
Effects
| Kind | Effect | What it does |
|---|---|---|
| Reveal | Rise |
Cascade in from below (or any side), Rise.fade, Rise.pop |
| Reveal | Glint |
Grow + fade in under a travelling sheen |
| Reveal | Blur |
Resolve out of a blur (word/line units) |
| Reveal | Typewriter |
Hard-cut, one unit at a time, optional blinking cursor |
| Reveal | Bounce |
Drop in from above, land with one soft overshoot and squash |
| Reveal | Squeeze |
Wide and flat, stretching upright as it lands (a stamp) |
| Reveal | Outline |
A stroke sweeps round each letter, then the fill rises inside it |
| Reveal | Scramble |
Random glyphs lock onto the real ones in order (the terminal) |
| Loop | Shimmer |
A light band sweeps in reading order, letters lift under it |
| Loop | Sparkle |
A few small glints twinkle on the glyphs |
| Loop | Float |
A tiny slow drift, each letter a beat behind |
| Loop | Wave |
A rounded ripple runs through the line |
| Loop | Pulse |
A slow glow breathes through the ink, no motion |
| Loop | Spotlight |
The shimmer's band as light: the line sits dim, letters under it are full ink, optional focus outline |
| Loop | Flicker |
One or two letters stutter dark and recover (a loose sign) |
| Ink | GradientInk |
Gradient fill on a span, rainbow, optional flow |
| Ink | Tint |
Flat recolour of a span, no relayout |
| Decor | Highlight |
A rounded marker sweeps in behind a span |
| Decor | Underline |
A stroke draws on under (or strike through) a span |
Reveals play over KineticText.duration on mount, on a replayKey change,
or from a KineticController; pass progress: to drive them from a scroll
or a parent animation instead. Loops run on their own clock while mounted.
Morph
TextMorph diffs the two texts by unit: the shared prefix and suffix stay
(gliding to their new place when the width changes), the differing middle
leaves and arrives in the chosen style â sheen, slide, roll, fold
(split-flap), wipe or crossfade â and the box's width eases between the
two.
roll turns the changed letters over a drum: the old glyph curves away
(foreshortening and dimming as it leaves the drum's window), the new one
curves in from the other side on the same clock, up when the number in the
text grew and down when it shrank. Nothing is clipped, so no sliver of the
leaving glyph is ever left behind. Because it works on the shaped paragraph,
it keeps cursive scripts joined.
Ticker
TickerText is the odometer proper â the SwiftUI numericText feel. Each
character sits in a slot keyed from the right (or TickerAnchor.left for a
label) and owns a continuous wheel that chases its target every frame,
not a 0â1 timeline that restarts on each change. Set a new value mid-roll
and every wheel redirects from where it is; a digit that has to go from 2 to
7 passes 3, 4, 5 and 6 on the way, the shorter way round the 0â9 ring. The
width eases as characters come and go; new slots fade in, dropped slots fade
out. Pass a formatted string and a tabular-figure style:
TickerText('1,250,000', style: figure.copyWith(fontFeatures: const [FontFeature.tabularFigures()]));
Characters are laid out one per slot, so use it for figures and Latin
labels; for cursive scripts reach for TextMorph with MorphStyle.roll,
which turns the same drum from one shaped paragraph.
Your own effect
class Wobble extends TextEffect {
const Wobble();
@override
bool get continuous => true;
@override
void apply(TextFrame frame, UnitSlice slice) {
for (var k = 0; k < slice.length; k++) {
frame.pose(slice.units[k]).dy += 2 * sin(frame.time * 4 + k);
}
}
}
A TextFrame gives you the ShapedText (every unit's rect, line and range),
the one-shot progress, the loop time, and lets you add ink passes and
decor painters behind or over the glyphs.
Notes on cost
- Shaping happens once per text/style/width â never on a rebuild that only constructs a fresh effect list; a frame at rest with no ink pass is one plain paint.
- A moving letter is one clipped re-draw of the paragraph; a fading or blurred one costs a small layer. Staggers keep the moving set small.
Bluron every letter of a paragraph is a layer per letter â use word or line units.SpotlightandPulseare one ink pass per frame and no layer per letter;Flickerposes only the letters that flicker.Outlinelays the paragraph out a second time with a stroked ink â once, cached on the shaped text.Scramblesets its stand-in glyphs one at a time, so keep it to Latin text and figures.- Horizontal travel suits word units; letters have no slack to slide.
Names
The effect classes are short on purpose â Rise, Shimmer, Blur, Float,
Tint â because they read as a list: effects: const [Rise(), Shimmer()].
If another library in your file exports one of these names (package:shimmer
has a Shimmer), hide it on one side:
import 'package:kinetic_text/kinetic_text.dart' hide Shimmer;
Nothing else in the barrel collides with Flutter's own exports.
License
MIT â see LICENSE.
Libraries
- kinetic_text
- Kinetic typography for Flutter â per-letter, per-word and per-line text motion from one shaped paragraph.