Aaron Winston
Aaron helps lead content strategy at GitHub with a focus on everything developers need to know to stay ahead of what's next. Also, he still likes the em dash despite its newfound bad rap.
Learn how GitHub built an accessible, multi-terminal-safe ASCII animation for the Copilot CLI using custom tooling, ANSI color roles, and advanced terminal engineering.
Most people think ASCII art is simple, and a nostalgic remnant of the early internet. But when the GitHub Copilot CLI team asked for a small entrance banner for the new command-line experience, they discovered the opposite: An ASCII animation in a real-world terminal is one of the most constrained UI engineering problems you can take on.
Part of what makes this even more interesting is the moment we’re in. Over the past year, CLIs have seen a surge of investment as AI-assisted and agentic workflows move directly into the terminal. But unlike the web—where design systems, accessibility standards, and rendering models are well-established—the CLI world is still fragmented. Terminals behave differently, have few shared standards, and offer almost no consistent accessibility guidelines. That reality shaped every engineering decision in this project.
Different terminals interpret ANSI color codes differently. Screen readers treat fast-changing characters as noise. Layout engines vary. Buffers flicker. Some users override global colors for accessibility. Others throttle redraw speed. There is no canvas, no compositor, no consistent rendering model, and no standard animation framework.
So when an animated Copilot mascot flying into the terminal appeared, it looked playful. But behind it was serious engineering work, unexpected complexity, a custom design toolchain, and a tight pairing between a designer and a long-time CLI engineer.
That complexity only became fully visible once the system was built. In the end, animating a three-second ASCII banner required over 6,000 lines of TypeScript—most of it dedicated not to visuals, but to handling terminal inconsistencies, accessibility constraints, and maintainable rendering logic.
This is the technical story of how it came together.
Before diving into the build process, it’s worth calling out why this problem space is more advanced than it looks.
Unlike browsers (DOM), native apps (views), or graphics frameworks (GPU surfaces), terminals treat output as a stream of characters. There’s no native concept of:
Because of this, every “frame” has to be manually repainted using cursor movements and redraw commands. There’s no compositor smoothing anything over behind the scenes. Everything is stdout writes + ANSI control sequences.
ANSI escape codes like \x1b[35m (bright magenta) or \x1b[H (cursor home) behave differently across terminals—not just in how they render, but in whether they’re supported at all. Some environments (like Windows Command Prompt or older versions of PowerShell) have limited or no ANSI support without extra configuration.
But even in terminals that do support ANSI, the hardest part isn’t the cursor movement. It’s the colors.
When you’re building a CLI, you realistically have three approaches:
For the Copilot CLI animation, this meant treating color as a semantic system, not a literal one: Instead of committing specific RGB values, the team mapped high-level “roles” (eyes, goggles, shadow, border) to ANSI colors that degrade gracefully across different terminals and accessibility settings.
Terminals are used by developers with a wide range of visual abilities—not just blind users with screen readers, but also low-vision users, color-blind users, and anyone working in high-contrast or customized themes.
That means:
This is also why the Copilot CLI animation ended up behind an opt-in flag early on—accessibility constraints shaped the architecture from the start.
These constraints guided every decision in the Copilot CLI animation. The banner had to work when colors were overridden, when contrast was limited, and even when the animation itself wasn’t visible.
Ink lets you build terminal interfaces using React components, but:
Which meant animation logic had to be handcrafted.
There are tools for ASCII art, but virtually none for:
Even existing ANSI preview tools don’t simulate how different terminals remap colors or handle cursor updates, which makes accurate design iteration almost impossible without custom tooling. So the team had to build one.
Cameron Foxly (@cameronfoxly), a brand designer at GitHub with a background in animation, was asked to create a banner for the Copilot CLI.
“Normally, I’d build something in After Effects and hand off assets,” Cameron said. “But engineers didn’t have the time to manually translate animation frames into a CLI. And honestly, I wanted something more fun.”
He’d seen the static ASCII intro in Claude Code and knew Copilot deserved more personality.
The 3D Copilot mascot flying in to reveal the CLI logo felt right. But after attempting to create just one frame manually, the idea quickly ran into reality.
“It was a nightmare,” Cameron said. “If this is going to exist, I need to build my own tool.”
Cameron opened an empty repository in VS Code, and began asking GitHub Copilot for help scaffolding an animation MVP that could:
Within an hour, he had a working prototype that was monochrome, but functional.
Below is a simplified example variation of the frame loop logic Cameron prototyped:
import fs from "fs";
import readline from "readline";
/**
* Load ASCII frames from a directory.
*/
const frames = fs
.readdirSync("./frames")
.filter(f => f.endsWith(".txt"))
.map(f => fs.readFileSync(`./frames/${f}`, "utf8"));
let current = 0;
function render() {
// Move cursor to top-left of terminal
readline.cursorTo(process.stdout, 0, 0);
// Clear the screen below the cursor
readline.clearScreenDown(process.stdout);
// Write the current frame
process.stdout.write(frames[current]);
// Advance to next frame
current = (current + 1) % frames.length;
}
// 75ms = ~13fps. Higher can cause flicker in some terminals.
setInterval(render, 75);
This introduced the first major obstacle: color. The prototype worked in monochrome, but the moment color was added, inconsistencies across terminals—and accessibility constraints—became the dominant engineering problem.
The Copilot brand palette is vibrant and high-contrast, which is great for web but exceptionally challenging for terminals.
ANSI terminals support:
Even in 256-color mode, terminals remap colors based on:
Which means you can’t rely on exact hues. You have to design with variability in mind.
Cameron needed a way to paint characters with ANSI color roles while previewing how they look in different terminals.
He took a screenshot of the Wikipedia ANSI table, handed it to Copilot, and asked it to scaffold a palette UI for his tool.
A simplified version:
function applyColor(char, color) {
// Minimal example: real implementation needed support for roles,
// contrast testing, and multiple ANSI modes.
const codes = {
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m"
};
return `${codes[color]}${char}\x1b[0m`; // Reset after each char
}
This enabled Cameron to paint ANSI-colored ASCII like you would in Photoshop, one character at a time.
But now he had to export it into the real Copilot CLI codebase.
Ink is a React renderer for building CLIs using JSX components. Instead of writing to the DOM, components render to stdout.
Cameron asked Copilot to help generate an Ink component that would:
import React from "react";
import { Box, Text } from "ink";
/**
* Render a single ASCII frame.
*/
export const CopilotBanner = ({ frame }) => (
<Box flexDirection="column">
{frame.split("\n").map((line, i) => (
<Text key={i}>{line}</Text>
))}
</Box>
);
And a minimal animation wrapper:
export const AnimatedBanner = () => {
const [i, setI] = React.useState(0);
React.useEffect(() => {
const id = setInterval(() => setI(x => (x + 1) % frames.length), 75);
return () => clearInterval(id);
}, []);
return <CopilotBanner frame={frames[i]} />;
};
This gave Cameron the confidence to open a pull request (his first engineering pull request in nine years at GitHub).
“Copilot filled in syntax I didn’t know,” Cameron said. “But I still made all the architectural decisions.”
Now it was time for the engineering team to turn a prototype into something production-worthy.
Andy Feller (@andyfeller), a long-time GitHub engineer behind the GitHub CLI, partnered with Cameron to bring the animation into the Copilot CLI codebase.
Unlike browsers—which share rendering engines, accessibility APIs, and standards like WCAG—terminal environments are a patchwork of behaviors inherited from decades-old hardware like the VT100. There’s no DOM, no semantic structure, and only partial agreement on capabilities across terminals. This makes even “simple” UI design problems in the terminal uniquely challenging, especially as AI-driven workflows push CLIs into daily use for more developers.
“There’s no framework for terminal animations,” Andy explained. “We had to figure out how to do this without flickering, without breaking accessibility, and across wildly different terminals.”
Andy broke the engineering challenges into four broad categories:
Most terminals repaint the entire viewport when new content arrives. At the same time, CLIs come with a strict usability expectation: when developers run a command, they want to get to work immediately. Any animation that flickers, blocks input, or lingers too long actively degrades the experience.
This created a core tension the team had to resolve: how to introduce a brief, animated banner without slowing startup, stealing focus, or destabilizing the terminal render loop.
In practice, this was complicated by the fact that terminals behave differently under load. Some:
To avoid flicker while keeping the CLI responsive across popular terminals like iTerm2, Windows Terminal, and VS Code, the team had to carefully coordinate several interdependent concerns:
The result was an animation treated as a non-blocking, best-effort enhancement—visible when it could be rendered safely, but never at the expense of startup performance or usability.
“ANSI color consistency simply doesn’t exist,” Andy said.
Most modern terminals support 8-bit color, allowing CLIs to choose from 256 colors. However, how those colors are actually rendered varies widely based on terminal themes, OS settings, and user accessibility overrides. In practice, CLIs can’t rely on exact hues—or even consistent contrast—across environments.
The Copilot banner introduced an additional complexity: although it’s rendered using text characters, the block-letter Copilot logo functions as a graphical object, not readable body text. Under accessibility guidelines, non-text graphical elements have different contrast requirements than text, and they must remain perceivable without relying on fine detail or precise color matching.
To account for this, the team deliberately chose a minimal 4-bit ANSI palette—one of the few color modes most terminals allow users to customize—to ensure the animation remained legible under high-contrast themes, low-vision settings, and color overrides.
This meant the team had to:
Rather than encoding brand colors directly, the animation maps semantic roles—such as borders, eyes, highlights, and text—to ANSI color slots that terminals can reinterpret safely. This allows the banner to remain recognizable without assuming control over the user’s color environment.


Cameron’s prototype was a great starting point for Andy to incorporate into the Copilot CLI but it wasn’t without its challenges:
First, the animation was broken down into distinct animation elements that could be used to create separate light and dark themes:
type AnimationElements =
| "block_text"
| "block_shadow"
| "border"
| "eyes"
| "head"
| "goggles"
| "shine"
| "stars"
| "text";
type AnimationTheme = Record<AnimationElements, ANSIColors>;
const ANIMATION_ANSI_DARK: AnimationTheme = {
block_text: "cyan",
block_shadow: "white",
border: "white",
eyes: "greenBright",
head: "magentaBright",
goggles: "cyanBright",
shine: "whiteBright",
stars: "yellowBright",
text: "whiteBright",
};
const ANIMATION_ANSI_LIGHT: AnimationTheme = {
block_text: "blue",
block_shadow: "blackBright",
border: "blackBright",
eyes: "green",
head: "magenta",
goggles: "cyan",
shine: "whiteBright",
stars: "yellow",
text: "black",
};
Next, the overall animation and subsequent frames would capture content, color, duration needed to animate the banner:
interface AnimationFrame {
title: string;
duration: number;
content: string;
colors?: Record<string, AnimationElements>; // Map of "row,col" positions to animation elements
}
interface Animation {
metadata: {
id: string;
name: string;
description: string;
};
frames: AnimationFrame[];
}
Then, each animation frame was captured to separate frame content from stylistic and animation details, resulting in over 6,000 lines of TypeScript to safely animate three seconds of the Copilot logo across terminals with wildly different rendering and accessibility behaviors:
const frames: AnimationFrame[] = [
{
title: "Frame 1",
duration: 80,
content: `
┌┐
││
││
└┘`,
colors: {
"1,0": "border",
"1,1": "border",
"2,0": "border",
"2,1": "border",
"10,0": "border",
"10,1": "border",
"11,0": "border",
"11,1": "border",
},
},
{
title: "Frame 2",
duration: 80,
content: `
┌── ──┐
│ │
█▄▄▄
███▀█
███ ▐▌
███ ▐▌
▀▀█▌
▐ ▌
▐
│█▄▄▌ │
└▀▀▀ ──┘`,
colors: {
"1,0": "border",
"1,1": "border",
"1,2": "border",
"1,8": "border",
"1,9": "border",
"1,10": "border",
"2,0": "border",
"2,10": "border",
"3,1": "head",
"3,2": "head",
"3,3": "head",
"3,4": "head",
"4,1": "head",
"4,2": "head",
"4,3": "goggles",
"4,4": "goggles",
"4,5": "goggles",
"5,1": "head",
"5,2": "goggles",
"5,3": "goggles",
"5,5": "goggles",
"5,6": "goggles",
"6,1": "head",
"6,2": "goggles",
"6,3": "goggles",
"6,5": "goggles",
"6,6": "goggles",
"7,3": "goggles",
"7,4": "goggles",
"7,5": "goggles",
"7,6": "goggles",
"8,3": "eyes",
"8,5": "head",
"9,4": "head",
"10,0": "border",
"10,1": "head",
"10,2": "head",
"10,3": "head",
"10,4": "head",
"10,10": "border",
"11,0": "border",
"11,1": "head",
"11,2": "head",
"11,3": "head",
"11,8": "border",
"11,9": "border",
"11,10": "border",
},
},
Finally, each animation frame is rendered building segments of text based on consecutive color usage with the necessary ANSI escape codes:
{frameContent.map((line, rowIndex) => {
const truncatedLine = line.length > 80 ? line.substring(0, 80) : line;
const coloredChars = Array.from(truncatedLine).map((char, colIndex) => {
const color = getCharacterColor(rowIndex, colIndex, currentFrame, theme, hasDarkTerminalBackground);
return { char, color };
});
// Group consecutive characters with the same color
const segments: Array<{ text: string; color: string }> = [];
let currentSegment = { text: "", color: coloredChars[0]?.color || theme.COPILOT };
coloredChars.forEach(({ char, color }) => {
if (color === currentSegment.color) {
currentSegment.text += char;
} else {
if (currentSegment.text) segments.push(currentSegment);
currentSegment = { text: char, color };
}
});
if (currentSegment.text) segments.push(currentSegment);
return (
<Text key={rowIndex} wrap="truncate">
{segments.map((segment, segIndex) => (
<Text key={segIndex} color={segment.color}>
{segment.text}
</Text>
))}
</Text>
);
})}
The engineering team approached the banner with the same philosophy as the GitHub CLI’s accessibility work:
“CLI accessibility is under researched,” Andy noted. “We’ve learned a lot from users who are blind as well as users with low vision, and those lessons shaped this project.”
Because of this, the animation is opt-in and gated behind its own flag—so it’s not something developers see by default. And when developers run the CLI in –screen-reader mode, the banner is automatically skipped so no decorative characters or motion are sent to assistive technologies.
By the end of the refactor, the team had:
This pattern—storing frames as plain text, layering semantic roles, and applying themes at runtime—isn’t specific to Copilot. It’s a reusable approach for anyone building terminal UIs or animations.
A “simple ASCII banner” turned into:
“The most rewarding part was stepping into open source for the first time,” Cameron said. “With Copilot, I was able to build out my MVP ASCII animation tool into a full open source app at ascii-motion.app,. Someone fixed a typo in my README, and it made my day.”
As Andy pointed out, building accessible experiences for CLIs is still largely unexplored territory and far behind the tooling and standards available for the web.
Today, developers are already contributing to Cameron’s ASCII Motion tool, and the Copilot CLI team can ship new animations without rebuilding the system.
This is what building for the terminal demands: deep understanding of constraints, discipline around accessibility, and the willingness to invent tooling where none exists.
The GitHub Copilot CLI brings AI-assisted workflows directly into your terminal — including commands for explaining code, generating files, refactoring, testing, and navigating unfamiliar projects.
Explore the GitHub Copilot CLI and try interacting with Copilot directly from your terminal.
Now in technical preview, the GitHub Copilot SDK can plan, invoke tools, edit files, and run commands as a programmable layer you can use in any application.
Run tests, fix code, and get support—right in your workflow. Stay focused and let Copilot handle the busywork.