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;
}
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:
- Change the width from 100% to 0%
- Move the element vertically
- 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 {
/* ... */
animation:
type var(--timePerLine) linear infinite,
carriageReturn var(--time) steps(var(--lines)) var(--lines),
caret 0.5s steps(2) infinite;
}
Customization
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)
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.
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
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