Shots in this scene
Flip and SplitText
In one line
Flip animates any layout change (Phase 10's FLIP, packaged): record, change the DOM, play. SplitText cuts a heading into lines, words and letters you can animate, and keeps it readable for screen readers.
Think of it like this
A photographer's contact sheet. Photograph the table, move the cards however you like, photograph it again, and the editor slides each card from its old place to its new one. Flip is that editor. SplitText is scissors: it cuts a headline into pieces so each can move on its own.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Flip
- A GSAP plugin that animates layout changes: it records positions, you change the DOM, and it plays the difference.
- SplitText
- A GSAP plugin that wraps a text's lines, words and letters in elements you can animate.
- mask
- A wrapper with
overflow: cliparound a piece, so it can slide in from outside its box. - aria-label
- An attribute that gives an element the name a screen reader says. SplitText sets it to the full text.
Step by step
01Flip a layout and pause in the middle
In "Flip a list" press Row ⇄ column, pause and step. The four cards glide from the row to the column, and the track lists width, height, x and y (Flip also carries rotation and skewX, which stay 0 here) over 600 ms. Press Add: the new card grows from 0 to full size and fades in (onEnter), while the others make room. Press Hide first: that card shrinks and fades (onLeave) and the rest slide into its place. Show all brings it back the same way as Add.
02Read what SplitText did
In "Split the headline" the console says the h1's aria-label is the full sentence, there are 5 words and 23 letters, and each piece has aria-hidden="true". A screen reader reads the label once and skips the 28 pieces.
03Split it twice
Press Split again (one at a time, reading the console). The piece count stays 28 and the first split's letters are no longer on the page: the second split replaced them, so pressing Play now animates the detached letters (a track appears) while the visible ones do not move. With overwrite: false the count jumps to 74, because the new split wraps the old wrappers. Keep one split per element; re-split with revert() or autoSplit.
04Let the lines follow the width
Narrow the box holding a headline and it wraps to more lines. With autoSplit: true SplitText notices, splits again and runs onSplit again; if onSplit returns the tween, SplitText reverts it and starts the new one. In a test page this headline was 1 line wide and 3 lines narrow.
Try it yourself
- 1
Take the accessibility off
In "Split the headline" change
aria: "auto"toaria: "none". The console now printsaria-label on the h1: nulland the pieces have noaria-hidden(null). The page looks the same, but the heading no longer has a single name. We did not test real screen readers here: the accessibility attributes are what changed. - 2
Split only letters
Change
type: "words,chars"totype: "chars". The console sayswords: 0 | chars: 23and then fails reading the first word's attribute:split.words[0]is undefined, so ask only for the pieces you split.
Code & diagrams
One `flip()` helper wraps every change. Try all four buttons, then slow the timeline down and step through the first one.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
cards: 4 | Flip.getState saved the position and size of eachPress Play for the letters to rise. Then press Split again and read the console, and Revert.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
aria-label on the h1: "Ride the hills of Ardenvale"
words: 5 | chars: 23
a letter's aria-hidden: true | a word's: trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Rebuild the elements with innerHTML
Change the Reverse button's handler to rebuild the list from text: list.innerHTML = cards().reverse().map((c) => '<div class="c">' + c.textContent + '</div>').join("").
Myth vs fact
Myth
Splitting text into letters is only visual, so it cannot hurt accessibility.
Fact
Without aria, assistive technology can read a split heading letter by letter, or lose the sentence. SplitText's aria: "auto" keeps one name on the element.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Flip's default animates width and height to keep children undistorted, and those are layout cost in Chromium (Topic 6.2). Use
scale: truefor plain shapes, andobject-fitinstead of scaling images. - ▸
SplitText measures lines from the real layout, so fonts matter: a web font that loads after the split changes the lines.
autoSplit: truelistens todocument.fontsand the width, andonSplitbuilds the animation again with each split.
Remember this
- 1
The problem this solves: Phase 10 wrote FLIP by hand. Real pages need the awkward cases too: elements that enter or leave, nested children, scaled parents and interrupted moves, which
Fliphandles. Cutting text into letters by hand breaks screen readers, selection and line breaks;SplitTextdoes it and handles the accessibility. - 2
Flip in three lines.
const state = Flip.getState(targets)records each element's position and size (First). You change the DOM any way you like (Last).Flip.from(state, { duration: 0.6 })inverts and plays: each element appears at its old place and slides to the new one. It returns a timeline.onEnterruns for elements that were not in the state (a new card, or one that wasdisplay: none), andonLeavefor elements that are now hidden. Two details: passtargets: ".c"toFlip.fromso Flip looks the elements up again and finds the new one, and hide a leaving element (hidden,display: none) instead of removing it, because a removed node is gone before Flip can fade it. - 3
What Flip animates. By default Flip tweens
widthandheight(layout work, like Phase 10's slow version).scale: trueusesscaleinstead, which is cheap but stretches children, so use it only for plain boxes.absolute: truetakes elements out of the flow during the move so that neighbours close up correctly. Adddata-flip-idto elements that are recreated (a framework re-render) so Flip knows the new one is the old one. - 4
SplitText.
SplitText.create(el, { type: "lines,words,chars" })wraps pieces indivs and returns.lines,.wordsand.charsarrays.mask: "lines"adds an overflow-clip wrapper per line so a line can rise out of a slot.aria: "auto"(the default) puts the full text inaria-labelon the element and hides every piece witharia-hidden="true", so the heading is still read as one sentence. - 5
Splitting twice, and resizing. SplitText reverts an earlier split of the same element before splitting again (
overwritedefaults to true), so the first split's pieces leave the page and any tween on them animates nothing visible. Line splits depend on width:autoSplit: true(it only matters whentypeincludeslines; the defaulttypeis"chars,words,lines") re-splits when the width or the fonts change the lines, and expectsonSplitto return the animation so it can be reverted and restarted.revert()restores the original text.
Explain it without notes
How do you animate a list re-order with Flip, and what does absolute: true do?
What does SplitText's aria: "auto" do, and why is it needed?
Practice
Animate a filter: hide the cards that do not match with Flip so the remaining ones slide to close the gap, with the others fading out.
Split the hero headline by lines with a mask so each line rises into view, and make it re-split on resize.
Done when you can
You can flip a layout change with
getStateandfrom, including enter and leaveYou know why recreated elements need
data-flip-idYou split text with
ariahandled, and can revert it