You don’t need a library for the usual “We build websites / apps / stores” headline. The plain JavaScript below types each word, backspaces it, moves to the next, respects reduced-motion settings and gives screen readers the full sentence, in about 40 lines (633 bytes gzipped). Reach for a library when you need choreography, like moving the cursor back to fix a typo or typing into a form field. If you do, check the licence first, because the two most popular typing libraries are no longer free for commercial sites.
The vanilla version
There are three pieces. The HTML keeps a hidden full sentence for screen readers and marks the animated copy as hidden from them:
<h1>
<span class="sr-only">We build websites, mobile apps and online stores.</span>
<span aria-hidden="true">We build <span class="typed">websites.</span></span>
</h1>.typed {
display: inline-block;
min-width: 7.5em; /* about as wide as the longest word */
}
.typed::after { /* the cursor */
content: "";
display: inline-block;
width: 0.06em;
height: 0.9em;
margin-left: 0.05em;
vertical-align: -0.1em;
background: currentColor;
animation: blink 1s step-end infinite;
}
@keyframes blink { 50% { opacity: 0 } }
.typed.done::after { display: none; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}const segmenter = new Intl.Segmenter();
const letters = (s) => Array.from(segmenter.segment(s), (x) => x.segment);
function typewriter(el, words, { typeMs = 80, eraseMs = 40, holdMs = 1600, loops = Infinity } = {}) {
if (matchMedia("(prefers-reduced-motion: reduce)").matches) {
el.textContent = words[0];
el.classList.add("done");
return () => {};
}
let w = 0, n = 0, deleting = false, round = 0, timer;
function tick() {
const chars = letters(words[w]);
n += deleting ? -1 : 1;
el.textContent = chars.slice(0, n).join("");
let delay = deleting ? eraseMs : typeMs;
if (!deleting && n === chars.length) {
if (w === words.length - 1 && ++round >= loops) {
el.classList.add("done"); // finished: stop the cursor
return;
}
deleting = true;
delay = holdMs;
} else if (deleting && n === 0) {
deleting = false;
w = (w + 1) % words.length;
delay = 300;
}
timer = setTimeout(tick, delay);
}
el.textContent = "";
tick();
return () => clearTimeout(timer);
}
typewriter(
document.querySelector(".typed"),
["websites.", "mobile apps.", "online stores. 👩💻"],
{ loops: 1 }
);Change the words in the last call. typeMs and eraseMs are the gaps between keystrokes, holdMs is how long a finished word stays up, and loops is how many times to go through the list before stopping on the last word. Leave loops out to repeat forever. The function returns a stop() you can wire to a pause button.
I ran this in Chrome: it types, erases and replaces each word, stops with the cursor hidden after one pass, shows the first word straight away when reduced motion is on, and Chrome’s accessibility tree reads the heading as the one full sentence.
Why it’s written this way
The w3schools example that ranks first for this search adds each letter with innerHTML += txt.charAt(i). That makes the browser serialize and re-parse the element’s whole contents on every keystroke, and it wipes out anything you attached to child elements. Setting textContent to a slice of the word avoids both.
Slicing by string position breaks emoji. "online stores. 👩💻".slice(0, 16) returns half an emoji (a lone surrogate, which shows as a �), because that emoji is five JavaScript characters. Splitting with [...word] is better but still breaks it into three code points, so the typing would show a woman, then a woman plus an invisible joiner, then the technologist. Intl.Segmenter splits by what a reader sees as one character, so the emoji arrives in one keystroke.
For screen readers, text that changes every 80 ms is either read as fragments or ignored. The sr-only span gives the whole idea (“websites, mobile apps and online stores”) in one go, and aria-hidden keeps the moving part out of the way.
And it stops. WCAG 2.2.2 says anything that starts by itself, moves for more than five seconds and sits next to other content needs a way to pause or stop it. An infinite loop in a hero fails that unless you add a pause control. Ending on the last word after one or two passes is the cheapest way to comply, and I think it reads better anyway: the visitor ends up looking at a finished sentence, not a cursor chewing through it again.
What goes wrong
The headline changes width with every keystroke. If it’s centred, the whole line slides left and right as words grow and shrink. If it’s near the edge of a phone screen, a long word like “online stores.” wraps onto a second line and pushes everything below it down, then pulls it back up when the word is erased. That counts as layout shift, and it looks broken.
The min-width on .typed is the fix: set it to roughly the width of your longest word (7.5em here) so the box never shrinks. On narrow screens, also give the heading a min-height of two lines, or shorten the words. Left-aligned headlines hide the problem best, which is one reason so many of these heroes are left-aligned.
The other common bug is starting the loop twice, for example in a React component that runs its effect twice in development. You get two timers fighting over the same element and garbled letters. Keep the stop() the function returns and call it in your cleanup.
typed.js, TypeIt, Motion and GSAP compared
Prices and licences as listed on each project’s own site and npm on 10 November 2026. Sizes are what I measured by gzipping each library’s browser build, except Motion’s, which is the figure from its docs (the file is behind the paid login).
| Option | Licence now | Commercial site | Size, gzipped | Typing features |
|---|---|---|---|---|
| Vanilla (above) | Yours | Free | 0.6 KB | Type, backspace, rotate, stop after N loops |
| typed.js 3.0.0 | GPL-3.0 (MIT up to 2.1.0) | $12 one project, $39 unlimited | 3.3 KB | Rotate strings, smart backspace, pauses with ^1000, strings read from HTML |
| TypeIt 8.8.7 | GPL-3.0 | $9 one project, $29 unlimited | 4.7 KB | Chained steps: type, delete, move the cursor, pause, freeze, type into inputs |
Motion typewriter | Motion+ only | $399 once, per developer | 1.8 KB | Human-like speed variance, backspaces to the first difference, sets aria-label |
| GSAP TextPlugin 3.15.1 | GSAP Standard “No Charge” licence | Free | 28.4 KB core + 3.5 KB plugin | Replaces text one character at a time; cursor and loop are up to you |
| anime.js 4.5.0 | MIT | Free | Not measured | No typewriter; has splitText and scrambleText |
The licence change most tutorials missed
typed.js was MIT-licensed for most of its life, and most tutorials still describe it as free. Version 3.0.0, published to npm on 24 January 2026, is GPL-3.0, and the README now has a licensing table: the GPL version for personal and open-source projects, a Limited Commercial License for one commercial project and an Unlimited one for any number. Their Stripe checkout pages list them at US$12 and US$39, both with lifetime updates. Version 2.1.0 (November 2023) is still on npm under MIT, so pinning typed.js@2.1.0 keeps you on the old terms, without future fixes.
TypeIt has been GPL for years (GPL-3.0 since version 8.0 in August 2021). Its pricing section says it’s free for personal or open-source projects and sells a $9 Limited and a $29 Unlimited commercial licence. I’m not your lawyer, and GPL obligations on a website are something people argue about, but both authors are clear about what they expect from commercial users, and $9 to $39 is less than the time you’d spend arguing.
Motion’s typewriter isn’t in the free, MIT-licensed motion package; it ships in motion-plus, which comes with a Motion+ membership at $399 once per developer seat. GSAP went the other way: it now belongs to Webflow, and its Standard “No Charge” licence covers the library and its plugins, TextPlugin included, for any website. That licence isn’t open source. It forbids using GSAP inside no-code visual animation builders that compete with Webflow, which won’t affect a normal website.
One more thing I checked in the source: neither typed.js 3.0.0 nor TypeIt 8.8.7 looks at prefers-reduced-motion, and neither labels the animated element for screen readers. typed.js only sets aria-hidden, on its cursor. If you use either, add the hidden-sentence markup from the vanilla version and check the media query yourself before starting the animation.
Which one I’d pick
For a hero headline with a few rotating words, the vanilla version. A dependency for 40 lines of logic is a dependency you’ll have to update and, now, possibly pay for. I’d pick TypeIt when the animation is the point of the page, a fake terminal or a demo that types, deletes a word and corrects a typo, because its chained API is far easier than writing that state machine by hand, and $9 is cheap. If you already ship GSAP for other animations, TextPlugin costs you 3.5 KB and no licence fee. I wouldn’t start a new commercial project on typed.js 3 without buying the licence, and I wouldn’t buy Motion+ for the typewriter alone.
Here is typed.js for comparison, with the same three words (it adds its own cursor):
<script src="https://unpkg.com/typed.js@3.0.0/dist/typed.umd.js"></script>
<script>
new Typed(".typed", {
strings: ["websites.", "mobile apps.", "online stores."],
typeSpeed: 80,
backSpeed: 40,
loop: true,
});
</script>When a GIF is better
JavaScript only helps where it can run. Email clients don’t run scripts, GitHub READMEs and Notion pages strip them, and Google Slides has nowhere to put one. Some site builders also charge for custom code: Carrd only allows embedded code on its Pro plans. In all of those, the same animation works as an image, because a GIF is just a picture that moves.
The typing text GIF maker builds exactly this (fixed text, rotating words with backspace, a blinking cursor) and exports a GIF or an animated PNG. It has costs: the text in an image can’t be selected, translated or indexed, so you put the sentence in the alt text, and a GIF is usually heavier than 600 bytes of script. On your own website, use the code. Everywhere else, use the image. If your animation is a single line with no backspace, the CSS typing animation needs no JavaScript at all.