Issue №128 Summer 2026A journal of dev tools, libraries & small ideasUpdated weekly
frontend · 5 min read
frontendJune 15, 2026 · 5 min

typewriter-fx

typewriter-fx is a tiny, dependency-free React library for typewriter text effects with a cursor that actually behaves like a real one — solid while characters are landing, blinking only when the typing rests. It ships a drop-in `<Typewriter>` component for the common case and a headless `useTypewri

typewriter-fx — live demo screenshot
01
frontend

A typewriter cursor that actually behaves like a cursor

I have a soft spot for the typewriter effect. Done well, it's a tiny bit of motion that makes a hero section feel alive. Done badly, it's distracting — and most implementations I reached for were doing one specific thing badly.

The cursor.

The detail nobody gets right

Watch a real terminal, or the caret in your editor. When you type, the cursor is solid — it doesn't blink mid-keystroke. The blinking only kicks in when you stop and the cursor is just sitting there waiting for you. It's a small thing, but your brain has stared at text cursors for thousands of hours and it knows when one is wrong.

Almost every typewriter component I tried blinks the cursor on a fixed setInterval that runs the entire time, including while characters are landing. The result is a cursor that flickers in the middle of "typing", which no real device does. Once you notice it, you can't unsee it.

So I built typewriter-fx, and the cursor was the whole point.

What it does

It's a small React library with three exports:

  • <Typewriter> — the drop-in component. Give it a string or an array of strings.
  • useTypewriter — the headless hook, if you want to own the markup.
  • <Cursor> — a standalone, presentational blinking cursor.
import { Typewriter } from 'typewriter-fx';

<h1>
  I build <Typewriter words={['React libraries', 'tiny tools', 'side projects']} />
</h1>

Pass one string and it types once. Pass several and it types, holds, deletes, and moves to the next — looping forever or stopping after the last one. There's a humanize flag that adds a little random variance to each keystroke so it doesn't tick like a metronome, plus the usual knobs for type speed, delete speed, and pause duration. No animation engine, no CSS to import, no dependencies.

How the "realistic" cursor works

The trick is almost embarrassingly simple. The hook already tracks whether it's actively typing — call it isTyping — because it needs to know when to schedule the next character. The cursor just keys off that:

const cursorSolid = effectivelyDisabled || (smartCursor && isTyping);

useEffect(() => {
  setCursorVisible(true);
  if (cursorSolid) return;            // solid while typing — no blink
  const id = setInterval(
    () => setCursorVisible((v) => !v), // blink only when idle
    cursorBlinkSpeed,
  );
  return () => clearInterval(id);
}, [cursorSolid, cursorBlinkSpeed]);

When isTyping flips true, the effect re-runs, sees cursorSolid, forces the cursor on, and bails before starting an interval. When typing rests, the interval comes back and the cursor blinks. Because the dependency only changes when typing starts or stops — not on every keystroke — there's no thrash. That's the entire "realism" feature.

The bug that nearly shipped

Here's the one that cost me an evening. React's StrictMode (which every dev React app runs, and which the demo uses) intentionally simulates an unmount-and-remount in development to flush out effects that aren't cleanup-safe. If you set up a timer or an observer outside of useEffect — say, in a ref callback — the cleanup runs on the simulated unmount, but the setup never runs again on the remount. Your animation silently dies, and your tests in jsdom won't catch it because jsdom doesn't perform that dance.

The fix is a discipline, not a trick: every timer lives inside a useEffect. The typing animation is a single self-scheduling setTimeout chain created inside an effect, with a clearTimeout in the returned cleanup. The blink is a setInterval, also inside an effect, also cleaned up. When StrictMode tears the component down and rebuilds it, both effects re-run from scratch and everything keeps working. No dangling timers, no dead cursors.

The other subtlety was the words array. If you depend on the array itself in the effect, a caller passing a fresh ['a', 'b'] literal on every render restarts the animation constantly. So the effect depends on a serialized key of the contents instead, and only restarts when the words actually change.

Accessibility

A naive typewriter is hostile to screen readers — it announces text one stuttering letter at a time. So the animated text is aria-hidden, and the full message is rendered once as a stable, visually-hidden label. Assistive tech reads the whole sentence; everyone else sees the animation. And if the user prefers reduced motion, the effect is skipped entirely and the final text just appears.

Try it

pnpm add typewriter-fx

There's a live demo with every feature animating at once — cursor styles, speeds, looping, humanized timing, the headless hook — at https://typewriter-fx.vercel.app, and the source is on https://github.com/kea0811/typewriter-fx. It's MIT licensed and tiny.

What's next

A few ideas I'm sitting on: a cursor render-prop for fully custom carets, per-word speed overrides, and a paused control prop. If you have a use case the current API doesn't cover, open an issue — I'd rather grow it from real needs than guess.

For now, it does the one thing I wanted: it types, and the cursor behaves like a cursor.


End of essay

About the author
Er An Khoo