tool / typewriter effectcss · javascript · react

Typewriter effect generator

Make text appear one letter at a time behind a blinking caret: in pure CSS with steps(), as the reader scrolls with scrollText, or with a small JavaScript typer that deletes and retypes several phrases. Preview each one, then copy the CSS, HTML, JavaScript or React.

Ways
3
Carets
4
Typer
49 lines
scrollText
3.5 KB
width: 0 → 10ch · animation: typing 1s steps(10)letters shown · the width in ch051000.51time, as a share of the durationlinear: grows smoothlysteps(10): a letter a stept = 0.454.5ch4chat t = 0.45, what each one showslinearwidth 4.5chtypewritersteps(10)width 4chtypewriter
Fig. 1 — the line’s width over one run of the animation. A linear ramp grows it smoothly and, 0.45 of the way through, cuts a letter in half; steps(10) moves it one 1ch cell at a time, so every frame shows whole letters. That staircase is the typewriter.
01The tool

Make a typewriter effect

Pick a way to type, write your text (one phrase per line), set the speeds and the caret, and watch the preview: it runs the code below, written from the same settings with only what differs from each one’s defaults.

Way to type

No JavaScript: the line’s width grows one letter at a time with steps(), in a monospace font, and a border is the caret. One line, of a length the CSS is cut to.

Preview · the CSS itself, 25 letters

Hello, I am a typewriter.

Typing
25 × 80 ms = 2s, in steps(25)
One loop
2s + 1.5s held + 1s deleting + a short blank = 4.9s
Width
0 → 25ch, one 1ch step per letter
Text

3 phrases · the first is 25 characters. This way types the first line only.

Timing
Loop

The CSS types the line, holds it, deletes it, leaves a short blank and starts again.

Start
Caret
Style
Blink

A border on the line itself: overflow: hidden clips the letters, never the border.

CSS types one line, so this way uses the first. To type the others in turn, use the JS typer.

typewriter.css37 lines
/* Made with https://svg-scroll-draw.vercel.app/typewriter-effect-generator */
/* One line in a monospace font, where every letter is 1ch wide: the 25
   letters of “Hello, I am a typewriter.” are 25ch, and steps(25) uncovers one at a time. */
.typewriter {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* Up to 2rem, and small enough for all 25 letters to fit 90% of the screen
     (a monospace letter is about 0.6em wide). */
  font-size: min(2rem, 6vw);
  line-height: 1.2;
  letter-spacing: 0;
  width: 25ch;
  box-sizing: content-box;
  overflow: hidden;
  white-space: nowrap;
  border-right: 0.08em solid;
  animation:
    typing 4.9s steps(25) infinite,
    caret 1s step-end infinite;
}

/* 2s typing, 1.5s held, 1s deleting, 0.4s blank: 4.9s in all. */
@keyframes typing {
  0% { width: 0; }
  40.82%, 71.43% { width: 25ch; }
  91.84%, 100% { width: 0; }
}

@keyframes caret {
  50% { border-right-color: transparent; }
}

/* Less motion asked for: the whole line, still. */
@media (prefers-reduced-motion: reduce) {
  .typewriter {
    animation: none;
  }
}

No library. The CSS is cut to this line’s 25 letters: a new line needs it made again.

Run it
023 ways

Three ways to type, and when each fits

They look alike for a second and part company after that. CSS is the lightest and the most limited; scrollText ties the typing to the reader’s scroll instead of a clock; the typer is the only one that can change what it types.

AspectCSS onlyOn scroll · scrollTextJS typer
Moved byThe clock: a CSS animation.The scroll position, between a start and an end line across the window.The clock: setTimeout, one letter at a time.
TextOne line, cut to its character count.One text, of any length; it wraps.Several phrases, one after another.
FontMonospace only.Any.Any.
DeletesThe same line, on a loop.Only as the reader scrolls back, with once: false.Each phrase, at a speed of its own.
StartsOn load; when in view with a few lines of JavaScript.As its top passes 85% of the way down the window.On load, or when in view.
ScriptNone, or the few lines that wait for the scroll.scrollText, 3.5 KB gzipped, and a short function for the caret.49 lines, no library.
Screen readersRead the whole line: it is all in the HTML, only clipped.Read the hidden copy scrollText adds, once.Read a hidden copy in the markup, once.
Less motionThe whole line, still.The whole text, at once.Every phrase, still.
Use it forA short heading or a terminal line, with no script at all.A heading or a sentence that types itself as the reader reaches it.Rotating words in a hero; any text that changes.
The typer
Each phrase gets a short blank, then a letter every speed ms, a hold while it is whole, and a letter gone every eraseSpeed ms; then the next phrase. It shows the caret steady while letters come and go and blinks it in the pauses, as a real one does.
03steps()

How the CSS typewriter works

Three properties do the work. white-space: nowrap keeps the text on one line, overflow: hidden hides whatever is past the box’s right edge, and the width, animated from 0 to the text’s length, decides how much shows. In a monospace font every character is the same width, 1ch, so 25 characters are exactly 25ch, and steps(25) takes the width there in 25 jumps of 1ch: one whole letter each (fig. 1).

typewriter.css33 lines
/* One line in a monospace font, where every letter is 1ch wide: the 25
   letters of “Hello, I am a typewriter.” are 25ch, and steps(25) uncovers one at a time. */
.typewriter {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* Up to 2rem, and small enough for all 25 letters to fit 90% of the screen
     (a monospace letter is about 0.6em wide). */
  font-size: min(2rem, 6vw);
  line-height: 1.2;
  letter-spacing: 0;
  width: 25ch;
  box-sizing: content-box;
  overflow: hidden;
  white-space: nowrap;
  border-right: 0.08em solid;
  animation:
    typing 2s steps(25) both,
    caret 1s step-end 2s infinite;
}

@keyframes typing {
  from { width: 0; }
}

@keyframes caret {
  50% { border-right-color: transparent; }
}

/* Less motion asked for: the whole line, still. */
@media (prefers-reduced-motion: reduce) {
  .typewriter {
    animation: none;
  }
}

steps(N) on its own means steps(N, end): each jump comes at the end of its step, so the first letter shows one letter’s time after the start and the last at the very end. The caret is the element’s own right border, and overflow: hidden clips what is inside the border, never the border, so it rides along at the edge of the text. A second animation blinks it, with step-end timing so it switches instead of fading. On a loop, one @keyframes rule holds the whole cycle, typing, holding, deleting and a blank, and the step timing applies to each stretch between two keyframes.

One line
white-space: nowrap never wraps, so a line wider than the screen runs off it. The generated CSS sizes the font so all the letters fit 90% of the screen, taking a monospace letter as about 0.6em.
Monospace
ch is the width of the digit 0. In a monospace font that is every letter’s width; in any other, an i and a W differ, and the steps stop part way through letters.
Fixed count
The count is in two places, the width and steps(), so a different text needs both changed. That is why the generator writes the CSS for your line, and why its React component keeps its text fixed.
Spacing
letter-spacing adds to every letter, so N letters overrun Nch and the end is cut off: the rule sets it to 0. Emoji and Chinese, Japanese or Korean characters are about 2ch wide, which no count fixes.
The box
Under box-sizing: border-box, which many resets put on every element, the width would include the caret’s border and cut the last letter: the rule sets content-box.
04scrollText

Typing as the reader scrolls

scrollText’s progress runs from 0, when the text’s top reaches 85% of the way down the window, to 1, when it reaches 40%: its default trigger. With split: 'chars' every character but a space is a piece of its own, and each piece takes its own share of that progress, starting stagger × its place in line of the way through. With stagger at 1 ÷ the number of letters, the shares run one after another and never overlap, so letter i begins as progress passes i ÷ letters. The easing (t) => (t > 0 ? 1 : 0) turns each share into a switch, hidden until it begins and whole after, and from: { opacity: 0 } only fades, where the default would also move each letter 24px.

main.js32 lines
import { scrollText } from 'svg-scroll-draw/text';

const el = document.querySelector('.typed');

// One letter after another: scrollText makes a piece of every character but
// the spaces, and each piece gets 1 ÷ that many of the scroll.
const letters = el.textContent.split('').filter((c) => c !== ' ').length;

scrollText(el, {
  split: 'chars',
  stagger: 1 / letters,
  easing: (t) => (t > 0 ? 1 : 0), // each letter whole, as its share begins
  from: { opacity: 0 },
});

followCaret(el);

/** Keeps .is-caret on the last letter shown, or on the element while none is. */
function followCaret(el) {
  const place = () => {
    let last = null;
    for (const child of el.children) if (Number(child.style.opacity) > 0) last = child;
    for (const old of el.querySelectorAll('.is-caret')) old.classList.remove('is-caret');
    el.classList.toggle('is-caret', !last);
    last?.classList.add('is-caret');
  };
  // Each time scrollText changes a letter's opacity.
  const watch = new MutationObserver(place);
  watch.observe(el, { attributes: true, attributeFilter: ['style'], subtree: true });
  place();
  return () => watch.disconnect();
}
The caret
scrollText draws no caret and reports no step, only onComplete at the end. followCaret watches the letters’ style attributes with a MutationObserver, which fires each time scrollText changes a letter’s opacity, and moves the is-caret class onto the last letter shown; the CSS draws the caret on that letter’s ::after.
Whole words
split: 'chars' makes each letter an inline-block, and a line may break between any two inline-blocks, so in 2.23.3 a word can split across two lines. Letters that only fade need no box of their own: the generated CSS sets them back to inline, with !important to win over the inline style, and lets the spaces between them wrap again.
Emoji
In 2.23.3 split: 'chars' cuts the text into UTF-16 units, so an emoji, or any character outside the Basic Multilingual Plane, becomes two halves that each show as �. Leave such characters out of a scroll typewriter; the JS typer counts with Array.from and keeps them whole.
The window
scrollText follows the window’s scroll and has no option for another scroller. The preview above runs it in a box by handing it the progress with seek(), worked out from the box’s scroll with the same trigger arithmetic.
React
<ScrollText> from svg-scroll-draw/react takes the same options as props and rebuilds only when one of them changes; a new easing function on each render is called through, not rebuilt. Its module is marked use client, and so must be the component that hands it an easing function, since a function cannot be passed from a Server Component.
05Accessibility

Typing that everyone can read

Read once, in full, from the start. A screen reader should hear the sentence, not letters arriving. Each way here keeps the whole text in the page from the first moment: the CSS version only clips its line, so the text is all there; scrollText hides its letters with aria-hidden and adds one visually hidden copy of the whole text; and the typer writes into an aria-hidden span beside a hidden copy of every phrase. None of them is a live region (aria-live), the one thing that would make a screen reader announce each change.

A hidden copy, not an aria-label. ARIA does not allow aria-label on a <p>, a <span> or a <div>, and a screen reader may skip it there; with every letter hidden it would then read nothing. scrollText uses a hidden copy for that reason, and so does the typer. More on splitting text a screen reader can read.

Less motion. Readers who ask their system for reduced motion see the text whole and still. The CSS turns its animations off under prefers-reduced-motion; scrollText shows every letter at once, and switches to that if the setting is turned on during a visit; the typer shows every phrase. The caret stops blinking.

Five seconds. WCAG 2.2.2, Pause, Stop, Hide, asks that anything moving or blinking by itself for more than five seconds, beside other content, can be paused, stopped or hidden. A typer on a loop never stops, and a caret that blinks for ever counts as well. Type once instead (with the generator’s default text and speeds the typer is done after 12.72s), or wire the function typewriter() returns, which stops it, to a pause button, and choose a steady caret.

06Next

Use it in your project

No package
Only the scroll way imports anything. The CSS version is a stylesheet, and the typer is the function at the end of its own script.
077 questions

Typewriter effect questions

Q1

How do I make a typewriter effect in CSS?

Set the line in a monospace font with white-space: nowrap and overflow: hidden, and animate its width from 0 to its length in ch with steps() set to the same number: for a 25-character line, width: 25ch and animation: typing 2s steps(25) both, with @keyframes typing { from { width: 0 } }. Each step uncovers one whole letter, because in a monospace font every letter is 1ch wide. A border-right on the same element is the caret, and a second animation on its colour, with step-end timing, makes it blink. The generator above writes the whole rule for your line.

Q2

Why does my CSS typewriter cut letters in half or stop too early?

The width and the letters disagree. The number in steps() and in the width must be the exact character count, spaces included. The font must be monospace, because ch is the width of the digit 0 and only a monospace font makes every letter that wide. letter-spacing must be 0, since it widens every letter. Under box-sizing: border-box the caret’s border eats into the width, so the rule needs content-box. And emoji and Chinese, Japanese or Korean characters are about 2ch wide, which no count fixes: type those with JavaScript.

Q3

How do I make a typewriter effect in JavaScript?

Add one character at a time on a timer: write the first n characters of the phrase into an element, wait the typing speed, and do it again; when the phrase is whole, wait, then take characters away at the delete speed and start the next one. The generator’s JS typer is 49 lines with no library. Two details matter. Count characters with Array.from(text), not text.length, so an emoji is never cut in half. And write into an aria-hidden element beside a visually hidden copy of the whole text, so a screen reader reads the text once instead of every change.

Q4

How do I type, delete and retype several words?

Keep the words in an array and loop over it: type one, hold it, delete it letter by letter, then type the next, back to the first after the last. The JS typer above does exactly that, with a typing speed, a delete speed and a hold of its own, and loop: false to stop on the last word. CSS alone can type, delete and retype one line, but each different word needs its own width and step count, so rotating several words is a job for JavaScript.

Q5

How do I make text type itself as you scroll?

Tie each letter to its own share of the scroll. With svg-scroll-draw that is scrollText(el, { split: 'chars', stagger: 1 / letters, easing: (t) => (t > 0 ? 1 : 0), from: { opacity: 0 } }), where letters is the number of characters other than spaces: each letter appears whole as the reader scrolls through its share, between the text’s top reaching 85% of the way down the window and reaching 40%. Scrolling back leaves it typed, because once is on by default; once: false un-types it. The generator’s On scroll way writes this, with a caret that follows the last letter shown.

Q6

Is a typewriter effect accessible?

It can be, with three things. Screen readers must get the whole text once, from the start, not each letter as it appears: keep the full text in the page, hide the typed letters with aria-hidden beside a visually hidden copy, and never put them in an aria-live region. Readers who ask for reduced motion should see the text whole, which all three ways here do through prefers-reduced-motion. And text that keeps moving, or a caret that keeps blinking, for more than five seconds needs a way to pause or stop it under WCAG 2.2.2: type once, or give a looping typer a pause button.

Q7

How do I make a blinking cursor in CSS?

Draw the caret, then switch it on and off. For a CSS typewriter line it can be the element’s border-right; anywhere else, an empty ::after with display: inline-block, a width, a height of 1em and background: currentColor, pulled back by a negative margin of its own width so the text never moves. Then animate it with @keyframes caret { 50% { opacity: 0 } } over 1s, step-end, infinite: step-end holds each half still, so it blinks instead of fading. Turn the animation off under prefers-reduced-motion.

The rest of svg-scroll-draw 2.23.7: the docs, the text animation generator for words and letters that move, and the text fill on scroll generator.