Skip to main content
ansicode
Elixir

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_enabled app 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") or Bunt.puts([:red, "error", :reset]). Adds named colour aliases (:chartreuse, :burlywood3, etc.) for finer-grained SGR-256 control. Used by credo, ex_doc, and many mix task 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 / view architecture rendered to ANSI via the bundled termbox2 NIF. View functions return tree-of-elements that the runtime diffs and paints. Used for mix task dashboards, log viewers, in-house dev tools.

Idiomatic patterns

Direct IO.puts — \e literal works in Elixir strings
# 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")
Idiomatic IO.ANSI.format — atoms + binaries → iolist
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?()))
Capability gate — IO.ANSI.enabled? + NO_COLOR + FORCE_COLOR
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 + Owl box
# 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()

Related sequences

Other languages