Back to blog

Building an Animated ASCII Art Title with React

·4 min read
reacttypescriptanimationdesign

I wanted something more distinctive than a plain text title for my website. ASCII art felt like the right choice it has that retro-tech aesthetic while still being pure text. But static ASCII art felt lifeless, so I added a subtle glitch animation.

The ASCII Art

Unicode block characters let you create surprisingly detailed text art. Here's my name rendered with half-block characters:

▄▀█ █░░ █▀▀ ░░█ ▄▀█ █▄░█ █▀▄ █▀█ █▀█
█▀█ █▄▄ ██▄ █▄█ █▀█ █░▀█ █▄▀ █▀▄ █▄█

█▀▀ █▀█ █▀█ █▀ █ █▄░█ █▀█
█▄▄ █▄█ █▀▄ ▄█ █ █░▀█ █▄█

The characters , , , and are from the Unicode Block Elements range. They render consistently across systems because they're part of the standard Unicode set, not dependent on any particular font.

The Glitch Effect

The animation randomly replaces a few characters with other block characters, creating a brief "corruption" effect. The timing is randomized to feel organic rather than mechanical.

'use client';
 
import { useEffect, useState } from 'react';
 
const ASCII_ART = `▄▀█ █░░ █▀▀ ░░█ ▄▀█ █▄░█ █▀▄ █▀█ █▀█
█▀█ █▄▄ ██▄ █▄█ █▀█ █░▀█ █▄▀ █▀▄ █▄█
 
█▀▀ █▀█ █▀█ █▀ █ █▄░█ █▀█
█▄▄ █▄█ █▀▄ ▄█ █ █░▀█ █▄█`;
 
const GLITCH_CHARS = '░▒▓█▀▄▌▐│─';
 
export function AsciiTitle() {
  const [displayText, setDisplayText] = useState(ASCII_ART);
  const [isGlitching, setIsGlitching] = useState(false);
 
  useEffect(() => {
    const glitchInterval = setInterval(
      () => {
        // Only glitch 30% of the time for subtlety
        if (Math.random() > 0.7) {
          setIsGlitching(true);
 
          // Corrupt a few random characters
          const chars = ASCII_ART.split('');
          const glitchCount = Math.floor(Math.random() * 5) + 2;
 
          for (let i = 0; i < glitchCount; i++) {
            const randomIndex = Math.floor(Math.random() * chars.length);
            if (chars[randomIndex] !== '\n' && chars[randomIndex] !== ' ') {
              chars[randomIndex] = GLITCH_CHARS[
                Math.floor(Math.random() * GLITCH_CHARS.length)
              ];
            }
          }
 
          setDisplayText(chars.join(''));
 
          // Reset after a brief flash
          setTimeout(
            () => {
              setDisplayText(ASCII_ART);
              setIsGlitching(false);
            },
            50 + Math.random() * 100,
          );
        }
      },
      2000 + Math.random() * 3000,
    );
 
    return () => clearInterval(glitchInterval);
  }, []);
 
  return (
    <span
      className={`whitespace-pre transition-all duration-75 ${
        isGlitching ? 'text-cyan-400 opacity-90' : ''
      }`}
      style={{
        textShadow: isGlitching ? '2px 0 #f0f, -2px 0 #0ff' : 'none',
      }}
    >
      {displayText}
    </span>
  );
}

Key Design Decisions

Randomized intervals: The glitch triggers every 2-5 seconds with a 30% probability. This keeps the effect surprising rather than predictable.

Brief duration: Each glitch lasts only 50-150ms. Long enough to notice, short enough to feel like a genuine digital artifact.

Chromatic aberration: During a glitch, the text shifts to cyan with magenta and cyan shadows offset in opposite directions. This mimics the RGB channel separation you see in old CRT monitors or corrupted video signals.

Preserve structure: The glitch never touches newlines or spaces. This keeps the overall shape readable even during corruption.

Styling Considerations

The component needs a monospace font to maintain alignment:

<div className="font-mono text-[10px] sm:text-xs leading-[1.2]">
  <AsciiTitle />
</div>

The small font size (10-12px) works because block characters are visually dense. The tight line-height keeps the rows cohesive.

For accessibility, I wrap it with a screen-reader-only heading:

<h1 className="sr-only">Alejandro Corsino</h1>
<div className="font-mono text-[10px] sm:text-xs leading-[1.2]">
  <AsciiTitle />
</div>

Variations

You could adapt this approach in several ways:

  • Matrix rain: Instead of random replacement, cascade characters downward
  • Typing effect: Reveal characters one at a time with a cursor
  • Hover trigger: Only glitch when the user hovers over the text
  • Color cycling: Rotate through different glitch colors

The core technique manipulating a string array and briefly displaying the corrupted version works for any text-based effect.

Performance

The interval runs continuously, but since it only updates state occasionally and the DOM changes are minimal (just text content and a couple CSS properties), the performance impact is negligible. React's reconciliation handles this efficiently.

If you wanted to be extra careful, you could pause the animation when the component isn't visible using the Intersection Observer API.