ANSI escape codes in Elixir
Elixir ships an IO.ANSI module in the standard library — every named SGR and a handful of cursor / screen helpers are functions that return their byte form (IO.ANSI.red() returns "\e[31m"). The idiomatic path is IO.ANSI.format/2, which takes a list mixing atoms (the named escapes) and binaries (your text) and returns an iolist suitable for IO.puts/1 or IO.write/1 — e.g. IO.puts(IO.ANSI.format([:bright, :red, "error: ", :reset, "permission denied"])). IO.ANSI.enabled?/0 is the capability gate baked into stdlib: it honours the :elixir app env :ansi_enabled (set by iex to true on a tty, false on a pipe) plus the NO_COLOR env var.
For more than ad-hoc prints: Bunt (savonarola/bunt) sugars IO.ANSI with named colour tags inside the string itself — Bunt.puts([:red, "error: ", :default_color, "permission denied"]) or the inline form Bunt.puts("[red]error:[/red] permission denied"). Owl (fuelen/owl) is the modern full-output toolkit (boxes, tables, progress bars, multi-line spinners, ANSI links) — used by Phoenix-generated CLI tasks, mix extensions, and livebook startup output. For full-screen TUIs reach for Ratatouille (ndreynolds/ratatouille) — an Elm-style model / update / view architecture rendered to ANSI via termbox2.
Recommended libraries
- IO.ANSI
Stdlib module —
IO.ANSI.red/0,IO.ANSI.bright/0,IO.ANSI.format/2(mixes atoms + binaries into iolist),IO.ANSI.enabled?/0(capability gate that honours:elixir's:ansi_enabledapp env +NO_COLOR). All escape generators are pure functions returning binaries — zero runtime cost, zero deps. - Bunt
Tagged-string sugar on top of IO.ANSI —
Bunt.puts("[red]error:[/red] permission denied")orBunt.puts([:red, "error", :reset]). Adds named colour aliases (:chartreuse,:burlywood3, etc.) for finer-grained SGR-256 control. Used bycredo,ex_doc, and manymixtask UIs. - Owl
Modern full-output toolkit — boxes (
Owl.Box.new/2), tables (Owl.Table.new/2), progress bars (Owl.ProgressBar), multi-line spinners, ANSI links (Owl.Data.tag/2). Used by Phoenix-generated CLI tasks,mix livebook.install, and other recent Elixir-tool UIs. - Ratatouille
Full-screen TUI framework — Elm-style
model / update / viewarchitecture rendered to ANSI via the bundledtermbox2NIF. View functions return tree-of-elements that the runtime diffs and paints. Used formixtask dashboards, log viewers, in-house dev tools.
Idiomatic patterns
# Elixir string literals do support \e (= \x1b). All three forms are
# byte-equivalent: \e, \x1b, \u{1b}. IO.puts is byte-clean.
IO.puts("\e[1;31merror:\e[0m permission denied")
IO.puts("\x1b[1;32mok:\x1b[0m all 142 tests passed")IO.puts(IO.ANSI.format([:bright, :red, "error: ", :reset, "permission denied"]))
IO.puts(IO.ANSI.format([:green, "ok: ", :reset, "all 142 tests passed"]))
# IO.ANSI.format/2 takes an optional second arg: emit_codes? boolean.
# Pass IO.ANSI.enabled? so the same call works in pipes / CI / NO_COLOR.
IO.puts(IO.ANSI.format([:red, "error"], IO.ANSI.enabled?()))defmodule MyApp.Color do
# IO.ANSI.enabled? already honours :ansi_enabled + NO_COLOR. Layer
# FORCE_COLOR on top for CI runs that want colour despite no tty.
def enabled? do
cond do
System.get_env("NO_COLOR") -> false
System.get_env("FORCE_COLOR") -> true
true -> IO.ANSI.enabled?()
end
end
def paint(chunks) when is_list(chunks) do
IO.ANSI.format(chunks, enabled?())
end
end
IO.puts(MyApp.Color.paint([:bright, :red, "error: ", :reset, "no perm"]))# Truecolor — IO.ANSI has no helper for SGR \x1b[38;2;r;g;b m, but you
# can splice raw bytes into the format list. Or use Owl, which accepts
# RGB tuples directly.
# Hand-rolled truecolor:
fg_rgb = fn r, g, b -> "\e[38;2;#{r};#{g};#{b}m" end
IO.puts([fg_rgb.(203, 166, 247), "lavender truecolor", IO.ANSI.reset()])
# Owl box with truecolor border:
# {:owl, "~> 0.12"} in mix.exs deps
"Build complete — 142 tests, 0 failures"
|> Owl.Box.new(border_style: :double, title: " status ", padding: 1)
|> Owl.IO.puts()