ZetCode

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.

basic_ansi.dart
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.

detect_support.dart
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.

CodeEffectExample
0Reset all attributes\x1B[0m
1Bold / increased intensity\x1B[1m
2Dim / decreased intensity\x1B[2m
3Italic\x1B[3m
4Underline\x1B[4m
7Inverse (swap fg/bg)\x1B[7m
9Strikethrough\x1B[9m
22Normal intensity (off)\x1B[22m
23Not italic\x1B[23m
24Not 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.

text_attributes.dart
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.

ansi_extensions.dart
/// 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.

sixteen_colours.dart
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.

colour_chart_256.dart
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 ModeForeground SequenceBackground SequenceTotal Colours
4-bit (standard)\x1B[{30–37}m\x1B[{40–47}m16
8-bit (256)\x1B[38;5;{n}m\x1B[48;5;{n}m256
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.

true_colour_gradient.dart
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.

backgrounds.dart
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.

cursor_commands.dart
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.

coloured_logger.dart
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.

progress_bar.dart
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.

rainbow_text.dart
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:

strip_ansi.dart
/// 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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.