TermUI provides a comprehensive styling system for colors, text attributes, and themes.

Style Basics

Create styles using Style.new/1:

alias TermUI.Renderer.Style

# Basic style
style = Style.new(fg: :cyan, bg: :black)

# With attributes
style = Style.new(fg: :red, attrs: [:bold, :underline])

# Apply to text
text("Hello, World!", style)

Colors

Named Colors (16 colors)

Standard terminal colors supported everywhere:

ColorNormalBright
Black:black:bright_black
Red:red:bright_red
Green:green:bright_green
Yellow:yellow:bright_yellow
Blue:blue:bright_blue
Magenta:magenta:bright_magenta
Cyan:cyan:bright_cyan
White:white:bright_white
Style.new(fg: :cyan)
Style.new(fg: :bright_yellow, bg: :blue)

256-Color Palette

Extended palette for more color options:

# Color index 0-255
Style.new(fg: 196)       # Bright red
Style.new(bg: 236)       # Dark gray

Color ranges:

  • 0-15: Standard colors (same as named)
  • 16-231: 6×6×6 color cube
  • 232-255: Grayscale ramp

True Color (24-bit RGB)

Full RGB support on modern terminals:

Style.new(fg: {255, 128, 0})     # Orange
Style.new(bg: {30, 30, 30})      # Dark gray

Default Color

Use terminal's default foreground/background:

Style.new(fg: :default)
Style.new(bg: :default)

Text Attributes

Modify text appearance:

AttributeEffect
:boldBold/bright text
:dimDimmed/faint text
:italicItalic text
:underlineUnderlined text
:blinkBlinking text
:reverseSwap foreground/background
:hiddenHidden text
:strikethroughStrikethrough text
Style.new(attrs: [:bold])
Style.new(attrs: [:bold, :underline])
Style.new(fg: :red, attrs: [:bold, :italic])

Note: Not all terminals support all attributes. bold, underline, and reverse have the widest support.

Fluent API

Build styles with method chaining:

style = Style.new()
  |> Style.fg(:blue)
  |> Style.bg(:white)
  |> Style.bold()
  |> Style.underline()

Available methods:

  • Style.fg(style, color) - Set foreground
  • Style.bg(style, color) - Set background
  • Style.bold(style) - Add bold
  • Style.dim(style) - Add dim
  • Style.italic(style) - Add italic
  • Style.underline(style) - Add underline
  • Style.blink(style) - Add blink
  • Style.reverse(style) - Add reverse
  • Style.hidden(style) - Add hidden
  • Style.strikethrough(style) - Add strikethrough

Style Merging

Combine styles with later values overriding earlier:

base = Style.new(fg: :white, bg: :black)
highlight = Style.new(fg: :yellow, attrs: [:bold])

merged = Style.merge(base, highlight)
# Result: fg: :yellow, bg: :black, attrs: [:bold]

Using Styles in Views

Styled Text

def view(state) do
  title_style = Style.new(fg: :cyan, attrs: [:bold])
  body_style = Style.new(fg: :white)

  stack(:vertical, [
    text("My Application", title_style),
    text(""),
    text("Welcome!", body_style)
  ])
end

Conditional Styling

def view(state) do
  status_style = case state.status do
    :ok -> Style.new(fg: :green)
    :warning -> Style.new(fg: :yellow)
    :error -> Style.new(fg: :red, attrs: [:bold])
  end

  text("Status: #{state.status}", status_style)
end

Style Variables

Define reusable styles:

defmodule MyApp.Styles do
  alias TermUI.Renderer.Style

  def header, do: Style.new(fg: :cyan, attrs: [:bold])
  def label, do: Style.new(fg: :bright_black)
  def value, do: Style.new(fg: :white)
  def error, do: Style.new(fg: :red, attrs: [:bold])
  def success, do: Style.new(fg: :green)
  def selected, do: Style.new(fg: :black, bg: :cyan)
end

Usage:

alias MyApp.Styles

def view(state) do
  stack(:vertical, [
    text("Dashboard", Styles.header()),
    text("CPU:", Styles.label()),
    text("#{state.cpu}%", Styles.value())
  ])
end

Themes

Create theme maps for consistent styling:

defmodule MyApp.Theme do
  alias TermUI.Renderer.Style

  def dark do
    %{
      header: Style.new(fg: :cyan, attrs: [:bold]),
      border: Style.new(fg: :cyan),
      text: Style.new(fg: :white),
      muted: Style.new(fg: :bright_black),
      selected: Style.new(fg: :black, bg: :cyan),
      error: Style.new(fg: :red),
      success: Style.new(fg: :green)
    }
  end

  def light do
    %{
      header: Style.new(fg: :blue, attrs: [:bold]),
      border: Style.new(fg: :blue),
      text: Style.new(fg: :black),
      muted: Style.new(fg: :bright_black),
      selected: Style.new(fg: :white, bg: :blue),
      error: Style.new(fg: :red),
      success: Style.new(fg: :green)
    }
  end
end

Usage with theme switching:

def init(_opts) do
  %{theme: :dark}
end

def event_to_msg(%Event.Key{key: "t"}, _state), do: {:msg, :toggle_theme}

def update(:toggle_theme, state) do
  new_theme = if state.theme == :dark, do: :light, else: :dark
  {%{state | theme: new_theme}, []}
end

def view(state) do
  theme = case state.theme do
    :dark -> MyApp.Theme.dark()
    :light -> MyApp.Theme.light()
  end

  stack(:vertical, [
    text("My App", theme.header),
    text("Press T to toggle theme", theme.muted)
  ])
end

Widget Styling

Widgets accept styles in their options:

alias TermUI.Widgets.Gauge

Gauge.render(
  value: 75,
  width: 20,
  style: Style.new(fg: :green)
)

Color Zones

Some widgets support color zones based on value:

Gauge.render(
  value: cpu_percent,
  width: 20,
  zones: [
    {0, Style.new(fg: :green)},    # 0-59%: green
    {60, Style.new(fg: :yellow)},  # 60-79%: yellow
    {80, Style.new(fg: :red)}      # 80-100%: red
  ]
)

Best Practices

1. Use Semantic Names

# Good - semantic meaning
error_style = Style.new(fg: :red)
success_style = Style.new(fg: :green)

# Avoid - color-focused
red_style = Style.new(fg: :red)

2. Consider Accessibility

  • Ensure sufficient contrast between foreground and background
  • Don't rely solely on color to convey information
  • Use bold/underline for emphasis in addition to color

3. Support Light and Dark

Design themes that work on both light and dark terminal backgrounds:

# Works on dark background
Style.new(fg: :white)

# Works on light background
Style.new(fg: :black)

# Works on both (terminal default)
Style.new(fg: :cyan)  # Typically visible on both

4. Minimize Style Changes

The renderer optimizes style changes, but fewer changes means better performance:

# Good - one style for the whole line
text("Label: Value", Style.new(fg: :white))

# Less efficient - multiple style changes
stack(:horizontal, [
  text("Label: ", Style.new(fg: :bright_black)),
  text("Value", Style.new(fg: :white))
])

Next Steps