DEV Community

Cover image for ASCII progress indicators
Frank Fiegel
Frank Fiegel

Posted on Originally published at glama.ai

ASCII progress indicators

I have always been fascinated by old terminal interfaces and wanted to bring some of their charm to the Glama chat UI. To indicate the progress of AI responses, I use ASCII progress indicators.

The animations are inspired by these two articles:

I transformed the concept into a React component.

Below is a collection of progress indicators:

ASCII progress indicators

Each animation is a sequence of characters that rotate at a set interval. For example, the characters ⣾, ⣽, ⣻, ⢿, ⡿, ⣟, ⣯, ⣷ become
 .

Here is the component code:

import { useEffect, useRef, useState } from 'react';

type ProgressIndicatorStyle = {
  frames: string[];
  interval: number;
};

const styles = {
  arrow: {
    frames: ['↑', '↗', '→', '↘', '↓', '↙', '←', '↖'],
    interval: 100,
  },
  ball_wave: {
    frames: ['𓃉𓃉𓃉', '𓃉𓃉∘', '𓃉∘°', '∘°∘', '°∘𓃉', '∘𓃉𓃉'],
    interval: 100,
  },
  blocks1: {
    frames: ['░', '▒', '▓', '█'],
    interval: 100,
  },
  blocks2: {
    frames: ['▛','▜','▟','▙'],
    interval: 100,
  },
  cym: {
    frames: ['⊏', '⊓', '⊐', '⊔'],
    interval: 100,
  },
  dots1: {
    frames: ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'],
    interval: 50,
  },
  dots2: {
    frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
    interval: 50,
  },
  dots3: {
    frames: ['⠋', '⠙', '⠚', '⠞', '⠖', '⠦', '⠴', '⠲', '⠳', '⠓'],
    interval: 50,
  },
  dots4: {
    frames: [
      '⠄',
      '⠆',
      '⠇',
      '⠋',
      '⠙',
      '⠸',
      '⠰',
      '⠠',
      '⠰',
      '⠸',
      '⠙',
      '⠋',
      '⠇',
      '⠆',
    ],
    interval: 50,
  },
  dots5: {
    frames: [
      '⠋',
      '⠙',
      '⠚',
      '⠒',
      '⠂',
      '⠂',
      '⠒',
      '⠲',
      '⠴',
      '⠦',
      '⠖',
      '⠒',
      '⠐',
      '⠐',
      '⠒',
      '⠓',
      '⠋',
    ],
    interval: 50,
  },
  dots6: {
    frames: [
      '⠁',
      '⠉',
      '⠙',
      '⠚',
      '⠒',
      '⠂',
      '⠂',
      '⠒',
      '⠲',
      '⠴',
      '⠤',
      '⠄',
      '⠄',
      '⠤',
      '⠴',
      '⠲',
      '⠒',
      '⠂',
      '⠂',
      '⠒',
      '⠚',
      '⠙',
      '⠉',
      '⠁',
    ],
    interval: 50,
  },
  dots7: {
    frames: [
      '⠈',
      '⠉',
      '⠋',
      '⠓',
      '⠒',
      '⠐',
      '⠐',
      '⠒',
      '⠖',
      '⠦',
      '⠤',
      '⠠',
      '⠠',
      '⠤',
      '⠦',
      '⠖',
      '⠒',
      '⠐',
      '⠐',
      '⠒',
      '⠓',
      '⠋',
      '⠉',
      '⠈',
    ],
    interval: 50,
  },
  dots8: {
    frames: [
      '⠁',
      '⠁',
      '⠉',
      '⠙',
      '⠚',
      '⠒',
      '⠂',
      '⠂',
      '⠒',
      '⠲',
      '⠴',
      '⠤',
      '⠄',
      '⠄',
      '⠤',
      '⠠',
      '⠠',
      '⠤',
      '⠦',
      '⠖',
      '⠒',
      '⠐',
      '⠐',
      '⠒',
      '⠓',
      '⠋',
      '⠉',
      '⠈',
      '⠈',
    ],
    interval: 50,
  },
  dots9: {
    frames: ['⢹', '⢺', '⢼', '⣸', '⣇', '⡧', '⡗', '⡏'],
    interval: 50,
  },
  dots10: {
    frames: ['⢄', '⢂', '⢁', '⡁', '⡈', '⡐', '⡠'],
    interval: 50,
  },
  dots11: {
    frames: ['⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'],
    interval: 50,
  },
  emoji_blink: {
    frames: ['😐', '😐', '😐', '😐', '😐', '😐', '😐', '😐', '😐', '😐', '😑'],
    interval: 100,
  },
  emoji_bomb: {
    frames: [
      '💣   ',
      ' 💣  ',
      '  💣 ',
      '   💣',
      '   💣',
      '   💣',
      '   💣',
      '   💣',
      '   💥',
      '    ',
      '    ',
    ],
    interval: 100,
  },
  emoji_earth: {
    frames: ['🌍', '🌎', '🌏'],
    interval: 200,
  },
  emoji_hour: {
    frames: [
      '🕛',
      '🕐',
      '🕑',
      '🕒',
      '🕓',
      '🕔',
      '🕕',
      '🕖',
      '🕗',
      '🕘',
      '🕙',
      '🕚',
    ],
    interval: 100,
  },
  emoji_moon: {
    frames: ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'],
    interval: 200,
  },
  line: {
    frames: ['☰', '☱', '☳', '☷', '☶', '☴'],
    interval: 100,
  },
  old: {
    frames: ['—', '\\', '|', '/'],
    interval: 100,
  },
  x_plus: {
    frames: ['×', '+'],
    interval: 100,
  },
} satisfies Record<string, ProgressIndicatorStyle>;

const useInterval = (callback: () => void, delay: null | number) => {
  const savedCallback = useRef(callback);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay === null) {
      return undefined;
    }

    const id = setInterval(() => savedCallback.current(), delay);

    return () => {
      clearInterval(id);
    };
  }, [delay]);
};

export const ProgressIndicator = ({
  style,
}: {
  style: keyof typeof styles;
}) => {
  const { frames, interval } = styles[style];

  if (!style) {
    throw new Error('Invalid style index');
  }

  const [index, setIndex] = useState<number>(0);

  useInterval(() => {
    setIndex((index + 1) % frames.length);
  }, interval);

  return (
    <div
      style={{
        color: '#00d992',
        fontFamily: 'monospace',
        pointerEvents: 'none',
        textAlign: 'center',
        userSelect: 'none',
        whiteSpace: 'pre',
        width: '24px',
      }}
    >
      {frames[index]}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)