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:
- https://odino.org/command-line-spinners-the-amazing-tale-of-modern-typewriters-and-digital-movies/
- https://medium.com/@Kaderovski/shell-loading-animations-990255ec415e
I transformed the concept into a React component.
Below is a collection of 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>
);
};
Top comments (0)