Reveals text line by line from the middle out, both halves of each line sliding apart from a seam.
<tp-mo-text-mirror>
Text mirror
Default density
Compact density
Restart from the initial state
Show the copyable source
Every review, every comment, in one shared timeline
Every review, every comment, in one shared timeline
<p style="margin:0;inline-size:32rem;max-inline-size:100%;font-size:2.75rem;line-height:1.05;font-weight:600;letter-spacing:-0.02em;text-align:center">
<tp-mo-text-mirror stagger="90" reveal-repeat>Every review, every comment, in one shared timeline</tp-mo-text-mirror>
</p>
Usage
Put Text mirror inside the heading or paragraph whose text it reveals and keep typography on that element; the lines inherit it. Each line is cut at the middle of its own text, whatever the alignment, and both halves slide out of that seam to their places. Lines are measured from the original text’s own layout, so the reveal never changes wrapping or height and nothing below moves. A standalone text reveals the first time it enters the viewport.
Inside a Scroll trigger that coordinator owns the timing.
Links and inline formatting keep working, and screen readers read the text once. At rest each line shows whole, so selection and links behave as in the original text. Under reduced motion every line appears at rest immediately and the events still fire.
Stagger
stagger is the delay in milliseconds between consecutive lines, and stagger-from starts the sequence at the first, last or center line.
From the center line
Default density
Compact density
Restart from the initial state
Show the copyable source
Plan the week, share the board, and keep every decision next to the work it changed, so nobody has to ask twice.
Plan the week, share the board, and keep every decision next to the work it changed, so nobody has to ask twice.
<p style="margin:0;inline-size:100%;max-inline-size:28rem;text-align:start;font-size:1.25rem;line-height:1.5;font-weight:500">
<tp-mo-text-mirror stagger="120" stagger-from="center" reveal-repeat>Plan the week, share the board, and keep every decision next to the work it changed, so nobody has to ask twice.</tp-mo-text-mirror>
</p>
reveal-repeat resets the text once it has fully left the viewport and plays again when it returns.
Orientation
orientation="vertical" cuts each line across the middle of its text instead, so its upper and lower halves slide apart from that seam.
Vertical seam
Default density
Compact density
Restart from the initial state
Show the copyable source
Plan the week and share the board with everyone
Plan the week and share the board with everyone
<p style="margin:0;inline-size:100%;max-inline-size:32rem;text-align:center;font-size:2.25rem;line-height:1.15;font-weight:600">
<tp-mo-text-mirror orientation="vertical" stagger="120" reveal-repeat>Plan the week and share the board with everyone</tp-mo-text-mirror>
</p>
Hiding and revealing again
reveal-hold keeps a ready text waiting with its halves closed. Setting it on a revealed text hides it, sliding each line back into its seam; removing it reveals the text again.
Hide and reveal
Default density
Compact density
Restart from the initial state
Show the copyable source
Every review, every comment and every release, in one shared timeline.
Hide
Every review, every comment and every release, in one shared timeline.
Hide
<div style="display:grid;gap:var(--tp-space-5);justify-items:center;inline-size:100%">
<p style="margin:0;inline-size:100%;max-inline-size:36rem;text-align:center;font-size:1.5rem;line-height:1.4;font-weight:600">
<tp-mo-text-mirror id="text-mirror-demo-hold" stagger="90">Every review, every comment and every release, in one shared timeline.</tp-mo-text-mirror>
</p>
<tp-button id="text-mirror-demo-toggle" variant="outline">Hide</tp-button>
</div>
<script type="module">
const text = document.querySelector('#text-mirror-demo-hold');
const toggle = document.querySelector('#text-mirror-demo-toggle');
toggle.addEventListener('click', () => {
text.revealHold = !text.revealHold;
toggle.textContent = text.revealHold ? 'Reveal' : 'Hide';
});
</script>
Scroll scrubbing
In a Scroll trigger with scrub, the scroll position slides the halves in both directions. Scroll the preview down and back up.
Scrubbed by the scroll
Default density
Compact density
Restart from the initial state
Show the copyable source
Every review, every comment and every release, in one shared timeline
Every review, every comment and every release, in one shared timeline
<div style="inline-size:100%;block-size:20rem;overflow:auto;border:var(--tp-border-width) solid var(--tp-border);border-radius:var(--tp-radius-lg);padding-inline:var(--tp-space-6)">
<div style="display:grid;gap:var(--tp-space-3);align-content:end;block-size:20rem;padding-block:var(--tp-space-6);box-sizing:border-box">
<tp-skeleton motion="none" style="block-size:0.75rem;inline-size:45%"></tp-skeleton>
<tp-skeleton motion="none" style="block-size:0.75rem;inline-size:90%"></tp-skeleton>
<tp-skeleton motion="none" style="block-size:6rem"></tp-skeleton>
</div>
<tp-scroll-trigger scrub scrub-range="cover">
<p style="margin:0;text-align:center;font-size:2rem;line-height:1.15;font-weight:600"><tp-mo-text-mirror stagger="120">Every review, every comment and every release, in one shared timeline</tp-mo-text-mirror></p>
</tp-scroll-trigger>
<div style="display:grid;gap:var(--tp-space-3);block-size:20rem;padding-block:var(--tp-space-6);box-sizing:border-box">
<tp-skeleton motion="none" style="block-size:0.75rem;inline-size:80%"></tp-skeleton>
<tp-skeleton motion="none" style="block-size:6rem"></tp-skeleton>
<tp-skeleton motion="none" style="block-size:0.75rem;inline-size:60%"></tp-skeleton>
</div>
</div>
API Reference
Every element also accepts motion-policy.
Attributes
AttributePropertyTypeDefaultDescriptionstaggerstaggernumber30Milliseconds between consecutive pieces.stagger-fromstaggerFromStaggerFrom'first'The piece the stagger starts from.reveal-repeatrevealRepeatbooleanfalseReveal again on every viewport entry instead of only the first.reveal-holdrevealHoldbooleanfalseWhile set, a ready reveal waits in its start state. Setting it on a revealed text hides it,
playing the reveal backwards; removing it reveals the text again.
Properties
PropertyTypeDefaultDescriptionlinesread-onlyHTMLElement[]—The line pieces, in order (empty unless lines are split).wordsread-onlyHTMLElement[]—The word pieces, in order.charsread-onlyHTMLElement[]—The character pieces, in order (empty unless characters are split).masksread-onlyHTMLElement[]—The mask wrappers, in order.revealedread-onlyboolean—Whether the reveal has played (false again after a repeat reset).
Events
EventTypeDescriptiontp-text-splitCustomEvent<{ lines: HTMLElement[]; words: HTMLElement[]; chars: HTMLElement[]; masks: HTMLElement[]; }>{ lines, words, chars, masks } after every split.tp-reveal-changeCustomEvent<{ revealed: boolean; effect: string; }>{ revealed, effect } when the reveal starts or a repeat resets it.tp-reveal-change-completeCustomEvent<{ revealed: boolean; }>{ revealed } after the last line settles.
Methods
MethodReturnsDescriptionsplitText()voidSplits again from the original content (after revert(), or to force a recalculation).revert()voidRestores the original content and stops splitting until splitText() is called.
Slots
SlotDescriptiondefaultThe text, with inline formatting and interactive elements.
CSS custom properties
PropertyDefaultDescription--tp-mo-text-mirror-duration—Per-line reveal duration. Default var(--tp-duration-slow).--tp-mo-text-mirror-easing—Reveal easing. Default var(--tp-easing-enter).--tp-mo-text-mirror-mask-bleed—How far each half's window extends over ascenders and descenders. Default 0.1em.