← Back to gallery
CSS

Multiline Typewriter Line-Step Sizing

Three CSS techniques for line-by-line text reveal — a max-height stack, a clip-path top-edge wipe, and a clip-path left-edge draw. Each row appears as a complete line-step, never a partial clip; the technique determines whether rows pop in vertically, wipe down, or draw left-to-right.

multilineline-stepmax-heightclip-pathstaggerno-layout-shiftprefers-reduced-motion

Multiline typewriter · line-step techniques

Multiline Typewriter Line-Step Sizing

Three different CSS techniques for revealing multi-line copy in line-step boundaries — max-height, clip-path inset (top edge), and clip-path inset (left edge). Each authored row appears as a whole line, never as a partial clip; the technique determines whether the row pops in, wipes down, or draws left-to-right.

max-height step · vertical expand

Stacked Reveal

Each row animates max-height from 0 → its line-height → hold → 0 so the row pops in vertically. The block min-height reserves the final size so the surrounding layout never shifts as rows appear.

  • max-height
  • no layout shift
  • vertical expand

clip-path inset (top) · curtain wipe

Edge Wipe

Each row animates clip-path: inset(0 0 100% 0) → inset(0) so it wipes in from the top edge like a curtain dropping. Layout stays stable; only the visible portion changes via clip-path.

  • clip-path
  • top-edge wipe
  • no layout shift

clip-path inset (left) · typewriter draw

Line Draw

Each row animates clip-path: inset(0 100% 0 0) → inset(0 0 0 0) so it draws left-to-right like a typewriter pen. The layout box stays reserved at full size; only the horizontal clip moves.

  • clip-path
  • left-edge draw
  • typewriter pass

Line-step inspector

Stacked Reveal

  • max-height
  • no layout shift
  • vertical expand

Each row animates max-height from 0 → its line-height → hold → 0 so the row pops in vertically. The block min-height reserves the final size so the surrounding layout never shifts as rows appear.

Helped you ship something? 🐟 Send my cat a churu

.line-step {
  display: grid;
  width: min(100%, 22ch);
  min-height: calc(3 * 1.5em);
  line-height: 1.35;
}

.line-step__row {
  display: block;
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  animation: line-step-stacked 3.40s ease-in-out infinite;
  animation-fill-mode: backwards;
}

.line-step__row:nth-of-type(1) { animation-delay: 0s; }
.line-step__row:nth-of-type(2) { animation-delay: 0.18s; }
.line-step__row:nth-of-type(3) { animation-delay: 0.36s; }
.line-step__row:nth-of-type(4) { animation-delay: 0.54s; }

@keyframes line-step-stacked {
  0%, 10%   { max-height: 0; opacity: 0; }
  18%, 82%  { max-height: 2em; opacity: 1; }
  92%, 100% { max-height: 0; opacity: 0; }
}

How to make this

A multiline typewriter reveal keeps each authored line in its own row, reserves the final block height, and animates row-level max-height or clip-path with staggered delays.

html
1<div class="line-step"  aria-label="Release motion primitives without layout drift.">3  <span class="line-step__row" aria-hidden="true">Release motion</span>  <span class="line-step__row" aria-hidden="true">primitives without</span>  <span class="line-step__row" aria-hidden="true">layout drift.</span></div> <style>.line-step {  display: grid;  align-content: center;  width: min(100%, 22ch);13  min-height: calc(3 * 1.5em);  color: #e0f2fe;  font: 700 1.1rem/1.35 sans-serif;  text-shadow: 0 0 22px rgba(103, 232, 249, .22);}.line-step__row {  display: block;20  clip-path: inset(0 100% 0 0);  animation: multiline-step-draw 3.4s ease-in-out infinite;22  animation-fill-mode: backwards;}.line-step__row:nth-of-type(1) { animation-delay: 0s; }.line-step__row:nth-of-type(2) { animation-delay: .18s; }.line-step__row:nth-of-type(3) { animation-delay: .36s; }@keyframes multiline-step-draw {  0%, 10% { clip-path: inset(0 100% 0 0); }  18%, 82% { clip-path: inset(0 0 0 0); }  92%, 100% { clip-path: inset(0 100% 0 0); }}@media (prefers-reduced-motion: reduce) {33  .line-step__row {    animation: none;    clip-path: none;  }}</style>

Annotated snippet

  1. Line 1The container owns a single accessible sentence while the animated row fragments are hidden from assistive technology. That avoids screen readers announcing clipped visual fragments as separate content.
    PitfallHow should multiline typewriter animation handle screen readers?

    Expose the full sentence once, either as normal DOM text or an aria-label, and mark purely visual duplicate row fragments aria-hidden. Screen readers should not have to track animated clipping or repeated row fragments to understand the message.

  2. Line 3Each line is authored as its own row. This is what makes the reveal step by line boundaries instead of slicing through wrapped text or half-visible glyphs.
    PitfallWhy does my multiline typewriter reveal cut words in half?

    You are probably animating one paragraph or one changing width. Author each intended line as its own row, then animate the visibility of each row. The browser can wrap text differently across viewport widths, so line-step animations need explicit row boundaries.

  3. Line 13The block reserves its final height before animation starts. Without this, each revealed row pushes surrounding layout and the typewriter becomes a layout shift.

    A neighbor element below the typewriter stays put when the block reserves min-height up front; without it, each new row shoves the neighbor down.

    PitfallHow do I prevent layout shift in a multiline reveal?

    Reserve the final block height with min-height based on the number of rows and line-height. The animation should change visibility, not the surrounding layout footprint. For max-height variants, the outer block still needs enough reserved height.

  4. Line 20clip-path hides the right side of each row while keeping the row box in layout. For a vertical pop variant, this same row structure can use max-height instead.

    A whole paragraph reveal slices through wrapped text; row-based reveal preserves authored line boundaries.

    PitfallIs clip-path good for typewriter-style row reveals?

    For short text blocks, yes. It keeps the row box stable and only changes the visible region. Large amounts of clipped text can still cost paint work, so avoid running many long looping text reveals on the same page.

  5. Line 22animation-fill-mode: backwards holds the first keyframe during each row delay. Without it, delayed rows can flash visible before their animation begins.
  6. Line 33Reduced motion clears the clip and stops the loop. The full sentence remains visible immediately in the same layout footprint.

Other pitfalls

Which browsers support clip-path inset for text rows?
Modern Chromium, Firefox, and Safari support clip-path: inset() on regular elements. Older browsers may show the full text immediately. That fallback is acceptable if the content remains readable and the layout is already reserved.

Notes

Overview

Multiline text reveal where each line appears as a whole unit (never a partial clip) via three CSS techniques: a max-height stack that pops rows in vertically, a clip-path: inset() top-edge wipe, and a clip-path: inset() left-edge draw. Layout never shifts — every row reserves its space and only the visibility animates.

When to use it

Reach for line-step reveal on multi-line hero copy, editorial sub-heads, marketing taglines. Skip it for body paragraphs — users will scan ahead of the reveal and the effect becomes friction. Skip it on content shorter than two lines; the step boundary is the point of the pattern.

How it works

For the max-height variant, each line lives in its own wrapper <span class="line"> with display: block; overflow: hidden; max-height: 0. A staggered @keyframes animates max-height from 0 to a value larger than any plausible line (2lh) so each row pops fully in. For clip-path variants, each line wrapper starts with clip-path: inset(0 0 100% 0) (top wipe) or inset(0 100% 0 0) (left wipe) and animates the matching side to 0. Use per-line animation-delay for the stagger.

Production gotchas

Animating max-height to a generous fixed value works for short lines but blows out layout if the line wraps to two visual lines — cap each wrapper to a single line at the typography level (truncate or guarantee width). The clip-path approach is layout-safe across wrapping but does not animate clip-path in Safari before 14 — gate behind @supports (clip-path: inset(0)) if you support old iOS. Long lines on wide viewports may need a proportional animation-duration for the left-edge variant or the wipe feels rushed.

Accessibility

The text is real DOM throughout — screen readers announce all lines at once on page load, regardless of the visual reveal. This is correct behavior; the reveal is decorative. Under prefers-reduced-motion: reduce drop all delays and durations to zero so lines appear instantly. Verify final-state contrast on each line; nothing about the reveal pattern changes the legibility floor.

References

Implementation depth

A multiline typewriter should reveal whole lines, not partial rows clipped through the baseline. Reserve line boxes up front, then animate max-height or clip-path per wrapper so the layout does not reflow while the text appears.

The implementation risk is responsive wrapping. If the copy wraps differently at narrow widths, a hardcoded line height can cut descenders or expose half a line. Test the longest localized string before tuning delays.