How to Use Colored Output in Rust CLIs (colored, owo-colors)

Cli
Use the anstyle crate to apply ANSI color styles to Rust CLI output for better readability.

When gray text isn't enough

You're building a CLI tool. It parses arguments, runs the logic, and prints the result. The output works, but it looks like a terminal from 1985. Errors blend into success messages. Headers disappear in the noise. You want red for errors, green for success, and bold for emphasis. You could hardcode escape sequences like \x1b[31m, but that's error-prone, unreadable, and breaks on some systems. Rust has a better way.

The modern standard is the anstyle crate. It defines safe types for colors and styles. It's the same crate used by the Rust compiler itself for its error messages. You pair it with anstream to handle output streams, auto-detection, and compatibility. Together, they give you type-safe colors without the hex-code headache.

How terminals actually colorize

Terminals understand ANSI escape codes. These are special byte sequences that tell the terminal to change colors, bold text, or move the cursor. The sequence starts with an escape character, followed by brackets and numbers. For example, the code for green text is \x1b[32m. The code to reset formatting is \x1b[0m.

Rust crates wrap these sequences in safe types so you don't have to memorize codes or worry about buffer overflows. anstyle provides enums like AnsiColor and structs like Style. You build a style object, and the crate generates the correct bytes when you print. This approach keeps your code readable and lets the compiler catch mismatches.

Terminals speak ANSI. Rust gives you the dictionary.

Minimal example with anstyle

Start with anstyle to see how styles work. Add anstyle = "1.0" to your Cargo.toml. The crate provides Style for formatting and AnsiColor for the standard 16 terminal colors.

use anstyle::{AnsiColor, Color, Style};

/// Prints a styled message using anstyle types.
fn main() {
    // Build a style with green foreground.
    let style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)));

    // Apply the style. The {style:#} syntax resets the color after the text.
    println!("{style}This text is green{style:#}");
}

When you run this, Style::new() creates an empty style object. The fg_color method chains a foreground color. AnsiColor::Green maps to the standard ANSI code for green. The println! macro formats the string. The {style} placeholder inserts the escape sequence to turn on green. The {style:#} placeholder inserts the reset sequence. The terminal receives the bytes, switches to green, prints the text, and switches back.

The compiler ensures the style types match the format string. You get type safety for colors.

Realistic CLI output with anstream

Real CLIs need to detect whether they are running in a terminal or being piped to a file. If you pipe colored output to a file, the escape codes end up in the file and break tools like grep. You also need to respect the NO_COLOR environment variable. The anstream crate handles all of this.

Add anstream = "0.6" to your Cargo.toml. anstream re-exports anstyle types, so you often only need this dependency. It provides AutoStream, which wraps standard streams and handles auto-detection.

use anstream::{AutoStream, ColorChoice, stdout};
use anstyle::{AnsiColor, Color, Style};

/// Prints a styled error message respecting TTY and NO_COLOR.
fn print_error(msg: &str) {
    // AutoStream detects TTY and NO_COLOR automatically.
    let mut out = stdout();

    // Define a style for errors.
    let error_style = Style::new()
        .fg_color(Some(Color::Ansi(AnsiColor::Red)))
        .bold(true);

    // Set the color for the stream.
    if let Err(e) = out.set_color(&error_style) {
        eprintln!("Failed to set color: {}", e);
        return;
    }

    // Print with color active.
    write!(out, "Error: {msg}").unwrap();

    // Reset the color.
    out.reset_color().unwrap();
    writeln!(out).unwrap();
}

fn main() {
    print_error("Something went wrong.");
}

stdout() returns an AutoStream. It checks if stdout is connected to a terminal. It checks the NO_COLOR environment variable. If either condition fails, it disables colors automatically. You don't need to write detection logic. The set_color method applies the style to the stream. reset_color clears it. This pattern keeps your output clean in pipelines and respects user preferences.

The ecosystem convention is to use anstream for output and anstyle for types. anstream re-exports anstyle types, so you often just depend on anstream. The ColorChoice::Auto behavior is the safe default. It checks NO_COLOR, checks if stdout is a TTY, and falls back to no color if either condition fails.

Always route output through anstream. Manual TTY detection is a rabbit hole.

Pitfalls and conventions

If you use println! directly with anstyle styles, you bypass TTY detection. The colors will print even when piped. This breaks pipelines. Users who pipe your output to grep or a file will see garbage characters. Always use anstream streams for production CLIs.

Another pitfall is ignoring NO_COLOR. Users who set NO_COLOR=1 expect plain text. This is a community standard. anstream respects this by default. If you build your own detection, you must check this variable manually. Failing to do so annoys users who rely on clean output.

Windows legacy terminals can be tricky. Older Windows versions don't support ANSI codes natively. anstream handles this by enabling virtual terminal processing on Windows 10 and later. It falls back gracefully on older systems. You don't need to write platform-specific code.

If you try to use anstyle types with a non-colored stream without the right traits, you might hit E0277 (trait bound not satisfied). Ensure you are using anstream's stream types when you need auto-detection. The error usually means you're mixing raw std::io streams with styled output. Switch to AutoStream to fix it.

Respect NO_COLOR. Your users will thank you.

Decision: which crate to use

Use anstream for new CLI projects. It provides AutoStream, respects NO_COLOR, detects TTYs, and handles Windows compatibility automatically. It wraps anstyle types and is the standard in the Rust ecosystem. Crates like clap use anstream internally.

Use anstyle directly only when you need to construct styles without touching I/O. This happens in libraries that generate formatted strings for callers to print. Keep the I/O logic in the binary crate. Return styled strings or style objects, and let the binary handle the stream.

Avoid colored and owo-colors in new code. These crates were popular before anstyle existed. They lack the modern auto-detection patterns and are not used by core Rust tools. Stick to the anstyle/anstream family for consistency.

Stick to the standard. anstream is the choice.

Where to go next