Dart ANSI Terminal Colours
last modified May 29, 2026
Introduction
ANSI escape codes are special byte sequences that control terminal
formatting: colours, bold text, underlines, cursor positioning, and more.
Virtually all modern terminals — including Windows Terminal, macOS Terminal,
GNOME Terminal, iTerm2, and Konsole — support them. Dart programs can emit
these codes directly via print or stdout.write to
produce vibrant, readable, and interactive console output.
Every ANSI sequence begins with the escape character
(\x1B or \033) followed by an open bracket
[, one or more numeric parameters separated by semicolons,
and a final command letter. The most common command is m
(for SGR — Select Graphic Rendition), which controls text appearance.
void main() {
// \x1B starts every ANSI escape sequence.
// 31 sets red foreground; 0 resets all attributes.
print('\x1B[31mThis text is red.\x1B[0m');
print('\x1B[1mThis text is bold.\x1B[0m');
print('\x1B[4mThis text is underlined.\x1B[0m');
// Without the reset (0), styles persist into subsequent output.
print('\x1B[33mYellow starts here...');
print('...and continues on this line!\x1B[0m');
}
Always close your sequences with \x1B[0m to reset the terminal
back to its default state. Forgetting the reset is the most common source of
surprises — stray colours bleeding into unrelated output or even into the
shell prompt after your program exits.
import 'dart:io';
void main() {
// Dart's stdout object can tell you if ANSI is available.
if (stdout.supportsAnsiEscapes) {
print('\x1B[32m✓ Your terminal supports ANSI escape codes.\x1B[0m');
} else {
print('ANSI escape codes are not supported on this terminal.');
print('Try running this program in a modern terminal emulator.');
}
}
Checking stdout.supportsAnsiEscapes is good practice when writing
cross-platform CLI tools. On older Windows consoles without virtual terminal
processing enabled, this flag will be false.
Text Attributes
Beyond colour, the SGR command accepts codes for bold, italic, underline, strikethrough, and more. These can be combined by separating codes with semicolons.
| Code | Effect | Example |
|---|---|---|
| 0 | Reset all attributes | \x1B[0m |
| 1 | Bold / increased intensity | \x1B[1m |
| 2 | Dim / decreased intensity | \x1B[2m |
| 3 | Italic | \x1B[3m |
| 4 | Underline | \x1B[4m |
| 7 | Inverse (swap fg/bg) | \x1B[7m |
| 9 | Strikethrough | \x1B[9m |
| 22 | Normal intensity (off) | \x1B[22m |
| 23 | Not italic | \x1B[23m |
| 24 | Not underlined | \x1B[24m |
The table above lists the most widely supported SGR attribute codes. Support for italic and strikethrough varies slightly across terminals, but all major emulators handle them well today.
void main() {
print('\x1B[1mBold\x1B[0m '
'\x1B[3mItalic\x1B[0m '
'\x1B[4mUnderline\x1B[0m '
'\x1B[9mStrikethrough\x1B[0m');
// Combine multiple attributes at once.
print('\x1B[1;3;4mBold, italic, and underlined!\x1B[0m');
// Dim text is useful for secondary information.
print('\x1B[2mThis is dimmed, less prominent text.\x1B[0m');
// Inverse mode swaps foreground and background colours.
print('\x1B[7mINVERTED TEXT\x1B[0m ← swapped foreground & background');
}
Attributes chain naturally. The sequence \x1B[1;3;4m activates
bold, italic, and underline simultaneously — all are cleared by the single
\x1B[0m that follows.
Extension Methods for a Fluent API
Typing raw escape codes repeatedly is tedious and error-prone. Dart's extension methods let us add styling directly onto strings, creating a clean, chainable API.
/// Extension that wraps a string with ANSI SGR codes.
extension AnsiStyle on String {
String get reset => '\x1B[0m$this\x1B[0m';
String get bold => '\x1B[1m$this\x1B[0m';
String get dim => '\x1B[2m$this\x1B[0m';
String get italic => '\x1B[3m$this\x1B[0m';
String get underline => '\x1B[4m$this\x1B[0m';
String get inverse => '\x1B[7m$this\x1B[0m';
// Standard 16 foreground colours
String get black => '\x1B[30m$this\x1B[0m';
String get red => '\x1B[31m$this\x1B[0m';
String get green => '\x1B[32m$this\x1B[0m';
String get yellow => '\x1B[33m$this\x1B[0m';
String get blue => '\x1B[34m$this\x1B[0m';
String get magenta => '\x1B[35m$this\x1B[0m';
String get cyan => '\x1B[36m$this\x1B[0m';
String get white => '\x1B[37m$this\x1B[0m';
// Bright variants
String get brightBlack => '\x1B[90m$this\x1B[0m';
String get brightRed => '\x1B[91m$this\x1B[0m';
String get brightGreen => '\x1B[92m$this\x1B[0m';
String get brightYellow => '\x1B[93m$this\x1B[0m';
String get brightBlue => '\x1B[94m$this\x1B[0m';
String get brightMagenta => '\x1B[95m$this\x1B[0m';
String get brightCyan => '\x1B[96m$this\x1B[0m';
String get brightWhite => '\x1B[97m$this\x1B[0m';
}
void main() {
// Now styling reads naturally — no raw codes in sight.
print('Success!'.green.bold);
print('Warning:'.yellow.bold + ' disk space low.'.dim);
print('Error:'.red.bold.underline + ' connection refused.'.italic);
// Chain multiple styles with ease.
print('Critical Alert'.brightRed.bold.underline);
}
Extension methods transform the raw escape-code approach into a readable, self-documenting API. The fluent style makes it immediately obvious what each piece of output will look like, and the compiler ensures you cannot misspell a colour name.
Standard 16 Colours (4-bit)
The original ANSI specification defines 8 colours (black, red, green, yellow, blue, magenta, cyan, white), each available in a normal and a bright variant, giving 16 foreground colours. Codes 30–37 select normal colours; 90–97 select their bright counterparts.
import 'dart:io';
void main() {
const normalCodes = [30, 31, 32, 33, 34, 35, 36, 37];
const names = ['Black', 'Red', 'Green', 'Yellow',
'Blue', 'Magenta', 'Cyan', 'White'];
stdout.writeln('=== Normal colours (30–37) ===');
for (int i = 0; i < normalCodes.length; i++) {
stdout.write('\x1B[${normalCodes[i]}m ${names[i].padRight(10)} \x1B[0m');
}
stdout.writeln();
stdout.writeln('=== Bright colours (90–97) ===');
for (int i = 0; i < normalCodes.length; i++) {
final brightCode = normalCodes[i] + 60; // 90–97
stdout.write('\x1B[${brightCode}m Bright${names[i].padRight(6)} \x1B[0m');
}
stdout.writeln();
}
The 16-colour palette is the most portable choice. Every ANSI-capable terminal supports it, making it ideal for scripts and CLI tools that must run in the widest range of environments.
Extended 256 Colours (8-bit)
Many terminals also support an 8-bit palette of 256 colours. The foreground
sequence is \x1B[38;5;{n}m where n ranges from
0 to 255. The first 16 values (0–15) mirror the standard 4-bit colours.
Values 16–231 form a 6×6×6 RGB cube (216 colours), and 232–255 provide a
24-step greyscale ramp.
import 'dart:io';
void main() {
// Print the 216-colour cube as a grid of coloured squares.
stdout.writeln('256-colour palette (colours 16–231):');
for (int row = 0; row < 18; row++) {
for (int col = 0; col < 12; col++) {
final colourCode = 16 + row * 12 + col;
if (colourCode > 231) break;
// Use the colour as background for a visible swatch.
stdout.write('\x1B[48;5;${colourCode}m \x1B[0m');
}
stdout.writeln();
}
// Greyscale ramp (232–255)
stdout.writeln('\nGreyscale ramp (colours 232–255):');
for (int g = 232; g <= 255; g++) {
stdout.write('\x1B[48;5;${g}m \x1B[0m');
}
stdout.writeln();
}
Running this program paints a vibrant colour chart directly in your terminal. The 6×6×6 cube provides smooth gradients across red, green, and blue axes, while the greyscale ramp is excellent for subtle UI elements like separators or de-emphasised labels.
True Colour (24-bit RGB)
Modern terminals also support true colour — a full 24-bit RGB
palette with over 16 million colours. The foreground sequence is
\x1B[38;2;{r};{g};{b}m, where each channel ranges from 0 to 255.
| Colour Mode | Foreground Sequence | Background Sequence | Total Colours |
|---|---|---|---|
| 4-bit (standard) | \x1B[{30–37}m | \x1B[{40–47}m | 16 |
| 8-bit (256) | \x1B[38;5;{n}m | \x1B[48;5;{n}m | 256 |
| 24-bit (true) | \x1B[38;2;{r};{g};{b}m | \x1B[48;2;{r};{g};{b}m | ~16.7 million |
The table above summarises the three colour depth levels and their escape sequence formats.
import 'dart:io';
void main() {
const text = '◆ Dart True Colour Gradient ◆';
// Print each character with a smoothly interpolated colour.
for (int i = 0; i < text.length; i++) {
final t = i / (text.length - 1); // 0.0 to 1.0
// Gradient from vibrant purple (128,0,255) to warm orange (255,128,0).
final r = (128 + (255 - 128) * t).round();
final g = (0 + (128 - 0) * t).round();
final b = (255 + (0 - 255) * t).round();
stdout.write('\x1B[38;2;$r;$g;${b}m${text[i]}');
}
// Always reset after the gradient.
stdout.writeln('\x1B[0m');
// Print a second gradient in reverse.
const line = '▁▂▃▄▅▆▇█▇▆▅▄▃▂▁';
for (int i = 0; i < line.length; i++) {
final t = i / (line.length - 1);
final r = (255 * (1 - t)).round();
final g = (50 + 150 * t).round();
final b = (100 + 155 * t).round();
stdout.write('\x1B[38;2;$r;$g;${b}m${line[i]}');
}
stdout.writeln('\x1B[0m');
}
True colour is ideal for syntax highlighters, heat maps, gradient headers, and any output where subtle colour differences convey meaning. Support is near-universal in desktop terminals released after 2015. Older terminals or basic console environments may fall back to a simpler palette, so always provide a graceful degradation path when targeting unknown environments.
Background Colours
Background colours use codes 40–47 (standard), 100–107 (bright), or the
extended sequences 48;5;{n} and 48;2;{r};{g};{b}.
Foreground and background codes combine freely — just separate them with
semicolons.
import 'dart:io';
void main() {
// Standard background colours (40–47) with contrasting foreground.
stdout.writeln('Standard backgrounds:');
for (final bg in [40, 41, 42, 43, 44, 45, 46, 47]) {
// Use bright white (97) foreground for readability on dark backgrounds,
// and black (30) on light backgrounds like yellow and white.
final fg = (bg == 43 || bg == 47) ? 30 : 97;
stdout.write('\x1B[${fg};${bg}m TEXT \x1B[0m ');
}
stdout.writeln('\n');
// True-colour background with a contrasting foreground.
stdout.writeln('True-colour backgrounds:');
const colours = [
[220, 50, 50], // warm red
[50, 180, 80], // fresh green
[50, 100, 220], // strong blue
[240, 160, 30], // amber
[140, 70, 200], // purple
];
for (final rgb in colours) {
final r = rgb[0];
final g = rgb[1];
final b = rgb[2];
// Choose white or black foreground based on perceived brightness.
final luminance = 0.299 * r + 0.587 * g + 0.114 * b;
final fg = luminance > 150 ? 30 : 97;
stdout.write('\x1B[${fg};48;2;$r;$g;${b}m #$r.$g.$b \x1B[0m ');
}
stdout.writeln();
}
Choosing the right foreground colour for a given background is essential for readability. The example above calculates a simple luminance value to pick between black and white text automatically — a small touch that makes coloured output feel polished and professional.
Cursor Manipulation
ANSI codes can also move the cursor, clear the screen, and erase lines. These are the building blocks of terminal user interfaces (TUIs), spinners, progress indicators, and live-updating dashboards.
import 'dart:io';
import 'dart:async';
void main() async {
// Clear the entire screen and move cursor to home (1,1).
stdout.write('\x1B[2J\x1B[H');
stdout.writeln('Screen cleared! This is line 1.');
stdout.writeln('This is line 2.');
await Future.delayed(Duration(seconds: 1));
// Move cursor up 2 lines and overwrite.
stdout.write('\x1B[2A'); // up 2
stdout.write('\x1B[K'); // erase current line
stdout.writeln('This line was updated in place.');
stdout.write('\x1B[K'); // erase next line
stdout.writeln('So was this one!');
await Future.delayed(Duration(seconds: 1));
// Move cursor to column 10 on the current line.
stdout.write('\x1B[10G');
stdout.writeln('(starting at column 10)');
// Save and restore cursor position.
stdout.write('\x1B[s'); // save position
stdout.writeln('This line appears...');
stdout.write('\x1B[u'); // restore position
stdout.writeln('...then this overwrites from the saved spot.');
}
The example demonstrates several essential cursor commands.
\x1B[2J clears the entire screen, \x1B[H sends the
cursor home, \x1B[{n}A moves up n rows,
\x1B[K erases from the cursor to the end of the line, and
\x1B[s / \x1B[u save and restore cursor position.
Mastering these few sequences unlocks dynamic terminal interfaces.
Practical Example — Coloured Logger
A common real-world application is a logger that uses colour to make log levels instantly recognisable. Errors pop in red, warnings catch the eye in yellow, and debug lines recede in dim grey.
import 'dart:io';
/// A minimal coloured console logger.
class Logger {
static final bool _useColour = stdout.supportsAnsiEscapes;
static String _timestamp() {
final now = DateTime.now();
return '${now.hour.toString().padLeft(2, '0')}:'
'${now.minute.toString().padLeft(2, '0')}:'
'${now.second.toString().padLeft(2, '0')}';
}
static void info(String msg) {
final label = _useColour ? '\x1B[36m[INFO]\x1B[0m' : '[INFO]';
stdout.writeln('${_timestamp()} $label $msg');
}
static void success(String msg) {
final label = _useColour ? '\x1B[32m[ OK ]\x1B[0m' : '[ OK ]';
stdout.writeln('${_timestamp()} $label $msg');
}
static void warn(String msg) {
final label = _useColour
? '\x1B[1;33m[WARN]\x1B[0m'
: '[WARN]';
stdout.writeln('${_timestamp()} $label \x1B[33m$msg\x1B[0m');
}
static void error(String msg) {
final label = _useColour
? '\x1B[1;31m[ERR!]\x1B[0m'
: '[ERR!]';
stdout.writeln('${_timestamp()} $label \x1B[1;31m$msg\x1B[0m');
}
static void debug(String msg) {
if (!_useColour) {
stdout.writeln('${_timestamp()} [DBG] $msg');
return;
}
stdout.writeln('${_timestamp()} \x1B[2m[DBG] $msg\x1B[0m');
}
}
void main() {
Logger.info('Server is starting on port 8080...');
Logger.debug('Loading configuration from /etc/app/config.yaml');
Logger.success('Configuration loaded (42 keys).');
Logger.warn('SSL certificate expires in 14 days.');
Logger.error('Connection to database timed out after 30s.');
Logger.info('Server shut down gracefully.');
}
The logger gracefully degrades when colour is unavailable by checking
stdout.supportsAnsiEscapes. When piping output to a file or
running in a non-ANSI terminal, plain labels are used instead. This pattern
keeps your CLI tools portable without sacrificing aesthetics where they count.
Practical Example — Terminal Progress Bar
A progress bar is a classic terminal UI element. Using carriage return
(\r) to overwrite the same line and ANSI colours to indicate
status, we can build a sleek, self-contained progress indicator.
import 'dart:io';
import 'dart:async';
/// Draws an animated progress bar that fills from 0% to 100%.
Future<void> main() async {
const totalSteps = 40;
const barWidth = 30;
for (int step = 0; step <= totalSteps; step++) {
final fraction = step / totalSteps;
final filled = (barWidth * fraction).round();
final empty = barWidth - filled;
// Build the bar segments.
final barFilled = '█' * filled;
final barEmpty = '░' * empty;
// Pick a colour that transitions from yellow → green.
final r = (255 * fraction).round();
final g = 200;
final b = (100 * (1 - fraction)).round();
// Percentage display.
final percent = (fraction * 100).toStringAsFixed(1).padLeft(5);
// \r returns the cursor to the start of the line.
// \x1B[K erases any leftover characters from a previous, longer line.
stdout.write(
'\r\x1B[38;2;$r;$g;${b}m[$barFilled$barEmpty]\x1B[0m '
'\x1B[1m$percent%\x1B[0m\x1B[K',
);
await Future.delayed(Duration(milliseconds: 80));
}
// Final newline so the shell prompt starts on a fresh line.
stdout.writeln();
print('\x1B[1;32m✓ Task completed successfully!\x1B[0m');
}
The progress bar combines several techniques: carriage return for in-place
updating, true-colour foreground for a gradient fill, bold text for the
percentage, and \x1B[K to prevent ghost characters when the
bar shrinks. The result is a polished, professional-looking indicator with
no external dependencies.
Practical Example — Rainbow Text Printer
Sometimes terminal output should be purely fun. A rainbow printer cycles through the hue spectrum, turning plain text into a vibrant cascade of colour. This example also demonstrates how to wrap ANSI logic in a reusable function.
import 'dart:io';
import 'dart:math';
/// Convert HSL (hue in degrees, saturation & lightness 0–1) to RGB.
List<int> _hslToRgb(double h, double s, double l) {
final c = (1 - (2 * l - 1).abs()) * s;
final x = c * (1 - ((h / 60) % 2 - 1).abs());
final m = l - c / 2;
double r, g, b;
if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
return [
((r + m) * 255).round().clamp(0, 255),
((g + m) * 255).round().clamp(0, 255),
((b + m) * 255).round().clamp(0, 255),
];
}
/// Print text with each character coloured from a rotating hue spectrum.
void printRainbow(String text, {double saturation = 0.9, double lightness = 0.55}) {
for (int i = 0; i < text.length; i++) {
// Spread the full hue range (0–360) across the string length.
final hue = (i / text.length) * 360.0;
final rgb = _hslToRgb(hue, saturation, lightness);
stdout.write('\x1B[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${text[i]}');
}
stdout.writeln('\x1B[0m');
}
void main() {
printRainbow('Dart makes terminal colours delightful!');
printRainbow('══════════════════════════════════', saturation: 0.7, lightness: 0.65);
printRainbow(' ★ Rainbow text with HSL → RGB ★ ');
// Print a longer example with bold styling.
const message = 'ANSI + Dart = Pure Joy';
for (int i = 0; i < message.length; i++) {
final hue = (i / message.length) * 360.0;
final rgb = _hslToRgb(hue, 1.0, 0.5);
stdout.write('\x1B[1;38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${message[i]}');
}
stdout.writeln('\x1B[0m');
}
The rainbow printer uses an HSL-to-RGB conversion to walk the colour wheel
smoothly. Because hue is continuous (0°–360°), the transition from character
to character is seamless. The clamp call ensures rounding never
produces out-of-range channel values — a subtle defensive measure that
prevents malformed escape sequences.
Limitations and Compatibility
ANSI escape codes are widely supported, but a few caveats are worth keeping in mind:
- Terminal support varies. While 4-bit colours work
everywhere, 256-colour and true-colour modes may not render correctly
on older terminals, basic console windows, or some CI/CD log viewers.
Always check
stdout.supportsAnsiEscapesbefore emitting codes in cross-platform tools. - Piping disables colour by default. When a program's
output is piped to a file or another process,
supportsAnsiEscapesusually returnsfalse. Provide a--colour=alwaysflag if users need forced colour output. - No universal italic. Italic (code 3) is supported on most Unix terminals and Windows Terminal, but some environments render it as reverse video or ignore it entirely.
- Escape sequences are invisible bytes. When debugging raw output, coloured lines may look garbled in logs. Consider stripping ANSI codes before writing to log files, or use a dedicated logging library that handles this automatically.
- Performance. For high-volume output (thousands of
lines per second), constructing escape sequences on every
printcall adds measurable overhead. Batch output or cache precomputed style strings in performance-sensitive code.
/// Remove all ANSI escape sequences from a string.
String stripAnsi(String input) {
// This regex matches CSI sequences (ESC [ ... m) and other common patterns.
final ansiRegex = RegExp(r'\x1B\[[0-9;]*[a-zA-Z]');
return input.replaceAll(ansiRegex, '');
}
void main() {
final coloured = '\x1B[1;31mError:\x1B[0m something went wrong.';
final plain = stripAnsi(coloured);
print('With ANSI: $coloured');
print('Stripped: $plain');
// Output (visually): Stripped: Error: something went wrong.
}
A simple regex-based stripper is sufficient for most use cases. For production-grade log sanitisation, consider using a dedicated package or expanding the regex to cover cursor-movement and screen-clearing sequences as well.
Source
ANSI escape codes (Wikipedia), Dart stdout.supportsAnsiEscapes, ANSI escape code reference
In this tutorial we covered the essentials of ANSI terminal colours in Dart: how escape codes are structured, how to apply text attributes and colours at three bit depths, how to build a fluent extension-method API, how to manipulate the cursor, and how to assemble everything into practical utilities like coloured loggers, progress bars, and rainbow printers.
Author
List all Dart tutorials.