DEV Community

Cover image for Multiline CSS-only typewriter effect
Alvaro Montoro
Alvaro Montoro

Posted on

Multiline CSS-only typewriter effect

Another entry for the typewriter effect CSS challenge. And with a different approach from the ones that have participated (I think, @afif keep me honest here, it could be "close" to the one you did earlier, but using different elements and properties.)

How it works

The idea of this effect is having two different moving elements: the text container in itself and a pseudo-element used to hide the content.

The container animation is simple: it grows a given height (the specified line height) until all the text has been displayed or the container reaches a limit of lines (500 by default). It happens in steps, so each line is revealed at a time.

@keyframes grow {
  0% { max-height: var(--lineHeight); }
  100% { max-height: calc(var(--lineHeight) * var(--lines)); }

.typewriter {
  /* ... */
  animation: grow var(--time) steps(var(--lines));
  animation-fill-mode: forwards;
Enter fullscreen mode Exit fullscreen mode

The pseudo-element has the same width as the container and a height equal to the line height. It reduces to a width of 0 (revealing the text as it shrinks) and then "jumps to the next line."

The animation of the pseudo-element is a little bit more complex... mainly because it is not an animation but three small animations together:

  1. Change the width from 100% to 0%
  2. Move the element vertically
  3. Animate the caret (to blink)
@keyframes carriageReturn {
  0% { top: 0; }
  100% { top: calc(var(--lineHeight) * var(--lines)); }

@keyframes type {
  0% { width: 100%; }
  100% { width: 0%; }

@keyframes caret {
  0% { color: var(--bgColor); }
  100% { color: black; }

.keyframes::before {
  /* ... */
    type var(--timePerLine) linear infinite, 
    carriageReturn var(--time) steps(var(--lines)) var(--lines),
    caret 0.5s steps(2) infinite;
Enter fullscreen mode Exit fullscreen mode


One thing I like about this solution is that it is highly customizable. The .typewriter class defines some default values for custom properties that the user can override. Here are the Properties:

Property Type Default Description
--bgColor Color White Defines the background color of the element and the animation
--lines Number 500 Maximum number of lines to animate
--lineHeight Length 1.5rem The line-height which will determine the size of the container height increase
--timePerLine Duration 4s The time that it will take for a line to be revealed
--widthCh Number 22 The width of the element in ch units (useful when used with monospace)
--width Length --widthCh * 1ch Optional. If you use --widthCh, there's no need to define this variable. But it is convenient to provide relative values.

There is one more custom property: --time, but that one is auto-calculated based on the number of lines and the time per line, and the users should not modify it.

On top of that, there are a series of classes that can be added to the container in HTML and that will provide some additional features:

  • monospace: makes the font as the default monospace family.
  • no-caret: removes the caret (convenient to avoid the ugly end-of-line animation)
  • big-caret: to display a wide caret instead of a thin one.

Pros and cons

Pros of this approach:

  • Fully multiline: works with any number of lines (define the max in the custom property --lines).
  • Responsive: users can define a width in characters or units, but the animation uses %, so it adapts to any size.
  • Font-friendly: it works with monospace and non-monospace fonts (but in reality, it looks better in monospace).
  • Highly customizable: Add a class to the typewriter element, or redefine the variables for different effects.
  • (Slightly more) accessible (than my previous entries): All the text is in place at the beginning so that ATs can detect it. Plus, it uses common CSS properties that are supported in most browsers.

Cons of this approach:

  • Not a polished finish: the caret goes until the end of each line, which looks weird (especially in the last line). The no-caret class removes the caret.
  • Content shift: if it's not absolutely positioned, the container will push the content below with each line that pops up.
  • Required styles: the animation requires all lines to have the same height, so a line-height value is needed. It's "vertically monospaced."
  • Limited backgrounds: the background must be a solid color. Otherwise, the animation of the pseudo-element will be revealed.
  • Responsive but not clean: the animations adapt to the element's width, but if the width is not specific, the letters may be cut off, and the animation won't be clean.
  • Scrolling: if the user selects the text, they could scroll the container. This could be avoided with user-select: none, but that could have some usability/accessibility issues of its own.

There are probably more cons, but these are the ones that I could think of at the moment... But definitely, there will be more.

@inhuofficial, this time I tested on iOS, and it works there too! 😊

Top comments (3)

afif profile image
Temani Afif

This is becoming more a duel than a war! I have to think about another entry

By the way, I think playing with some masks can get rid of the background coloration you need.

alvaromontoro profile image
Alvaro Montoro

That would be cool. I wonder if setting the background to white and then picking a blend mode of dark will always pick the background by default... but I'm assume it won't hide the text either :S

afif profile image
Temani Afif

yes, if you apply mix-blend-mode: darken; to only the .typewriter element it will work but your text need to be "darker" than the background color of your section or body. It's perfect if your text is always black