Quick lookup for every built-in widget and helper. One example per struct, key fields inline. For concepts see Building UIs; for full option shapes, the module docs.

Styles & rich text

Style

%Style{
  fg: :green,                         # or {r,g,b}, or 0..255 index
  bg: :reset,
  underline_color: {:rgb, 255, 0, 0}, # colored underline (kitty/wezterm)
  modifiers: [:bold, :italic, :underlined, :reversed, :dim]
}

Colors accept :reset, named atoms, {r, g, b}, or 0–255. Modifiers are additive.

Rich text spans/lines

alias ExRatatui.Text.{Line, Span}

Line.new([
  Span.new(" ok ", style: %Style{fg: :green, modifiers: [:bold]}),
  Span.new(" build passed", style: %Style{fg: :gray})
])

Accepted by Paragraph.text, List.items, Table.rows/header, Tabs.titles, Block.title.

Events

poll_event/1 returns

case ExRatatui.poll_event(timeout) do
  %Event.Key{code: "q"} -> :quit
  %Event.Mouse{kind: "down", button: "left", x: x, y: y} ->
    {:click, x, y}
  %Event.Resize{width: w, height: h} -> {:resize, w, h}
  %Event.Paste{content: text} -> {:paste, text}
  %Event.FocusGained{} -> :resume_animations
  %Event.FocusLost{} -> :pause_animations
  nil -> :no_event
end

Event.Paste arrives only on the local terminal today (byte-stream transports' VTE parser doesn't decode CSI 200~/201~ yet). Focus events are opt-in: ExRatatui.run(fun, focus_events: true).

Consuming Paste in widgets

case event do
  %Event.Paste{content: text} ->
    ExRatatui.text_input_insert_str(state.input, text)
  # ...
end

text_input_insert_str/2 strips every control character (single-line). textarea_insert_str/2 preserves \n and \r\n as line breaks; lone \r is dropped. Both batch the insert as one NIF call.

Layout

Layout.split/4

alias ExRatatui.Layout

[left, right] =
  Layout.split(frame_rect, :horizontal, [
    {:percentage, 30},
    {:percentage, 70}
  ])

Direction: :horizontal or :vertical. Constraints: :length, :percentage, :min, :max, :ratio, {:fill, weight}.

Flex + spacing + margin

# Centered fixed-width popup
[popup] = Layout.split(area, :horizontal,
  [{:length, 40}], flex: :center)

# Two growable panels with a 2-cell gutter
[a, b] = Layout.split(area, :horizontal,
  [{:fill, 1}, {:fill, 1}], spacing: 2)

# Split but leave a 1-cell border around the edges
[body] = Layout.split(area, :vertical, [{:min, 0}], margin: 1)

:flex is one of :legacy, :start, :end, :center, :space_between, :space_around. :spacing is cells between segments; :margin (or :horizontal_margin / :vertical_margin) insets the whole area before splitting.

%Rect{}

%ExRatatui.Layout.Rect{x: 0, y: 0, width: 80, height: 24}

All widgets render into a Rect. Returned from Layout.split/4, derived from %Frame{}, or built manually for absolute placement.

Padding helpers

alias ExRatatui.Layout.Padding

%Block{padding: Padding.uniform(1)}        # {1, 1, 1, 1}
%Block{padding: Padding.symmetric(2, 1)}   # {2, 2, 1, 1}  (horiz, vert)
%Block{padding: Padding.horizontal(2)}     # {2, 2, 0, 0}
%Block{padding: Padding.vertical(1)}       # {0, 0, 1, 1}
%Block{padding: Padding.new(1, 2, 3, 4)}   # {1, 2, 3, 4}

Block.padding is a {left, right, top, bottom} tuple; these just build it.

Focus & theming

Focus ring

alias ExRatatui.{Focus, Event}

# Build the ring once.
focus = Focus.new([:search, :results, :details])

# Register hit-test regions after layout (typically on resize).
focus =
  focus
  |> Focus.set_region(:search, search_rect)
  |> Focus.set_regions(%{results: results_rect, details: details_rect})

# Route keys: Tab / Shift+Tab move focus, everything else passes through.
{focus, key} = Focus.handle_key(focus, key_event)

# Route mouse: left-click in a region focuses that ID + passes event through.
{focus, mouse} = Focus.handle_mouse(focus, mouse_event)

# Style off the focused ID — Focus never touches widget structs.
border = if Focus.focused?(focus, :search),
  do: %Style{fg: :yellow},
  else: %Style{fg: :gray}

Focus.at(focus, x, y) returns the ID under a point (smallest region wins on overlap). Scroll routing is intentionally not built in — pick Focus.current/1 for "scroll the focused widget" or Focus.at/3 for "scroll the widget under the cursor".

Theme palette

alias ExRatatui.Theme

theme = Theme.default()                 # dark-friendly; surface nil
# or  Theme.light()                     # dark text on white surface
# or  %Theme{primary: :magenta, accent: {:rgb, 245, 158, 11}, ...}

%Block{
  borders: [:all],
  border_style: Theme.border_style(theme, focused: focused?)
}

%List{
  items: results,
  highlight_style: Theme.selection_style(theme)
}

# Body text + dim hint
Theme.text_style(theme)                 # %Style{fg: theme.text, bg: theme.surface}
Theme.text_style(theme, dim: true)      # %Style{fg: theme.text_dim, ...}

Eleven semantic slots: :primary, :accent, :border, :border_focused, :surface, :surface_alt, :text, :text_dim, :success, :warning, :danger. Each accepts any ExRatatui.Style.color/0. Apps thread the theme through render code by hand; no global config, no automatic widget injection.

Text & content

Paragraph

%Paragraph{
  text: "Hello!",                      # String | Span | Line | [Span]
  style: %Style{fg: :green},
  alignment: :center,                  # :left | :center | :right
  wrap: true,
  scroll: {0, 0},                      # {vertical, horizontal}
  block: %Block{title: " Info ", borders: [:all]}
}

Markdown

%Markdown{
  content: "# Title\n\n**bold** *italic* `code`\n\n- a\n- b",
  wrap: true,
  scroll: {0, 0},
  block: %Block{borders: [:all]}
}

Supports headings, bold/italic, inline code, lists.

BigText

ExRatatui.BigText.new("HELLO",
  pixel_size: :quadrant,               # :full | :half_height | :half_width
                                       # :quadrant | :third_height | :sextant
                                       # :quarter_height | :octant
  alignment: :center,                  # :left | :center | :right
  style: %Style{fg: :magenta},
  block: %Block{borders: [:all]}
)

Oversized 8×8-pixel text for slide titles / banners. Smaller pixel_size packs more characters into the same area; :octant is the densest. Backed by tui-big-text.

CodeBlock

%ExRatatui.Widgets.CodeBlock{
  content: source_code,
  language: "rust",                    # nil = plain text fallback
  theme: :base16_ocean_dark,           # 7 curated atoms or any raw string
  line_numbers: true,
  starting_line: 1,
  highlight_lines: [3, 7..9]           # ints + ranges, normalised
}

Syntax-highlighted source code, display-only. Themes (curated atoms): :base16_ocean_dark, :base16_ocean_light, :base16_eighties_dark, :base16_mocha_dark, :inspired_github, :solarized_dark, :solarized_light. Raw strings pass through for custom theme sets. Languages: syntect's bundled SyntaxSet (Rust, Python, JS, Ruby, Go, Java, JSON, YAML, Erlang, etc.) plus Elixir which we ship as an additional bundled syntax.

Raw highlighted lines for composite widgets:

ExRatatui.CodeBlock.highlight("fn main() {}", "rust", :solarized_dark)
# => [%ExRatatui.Text.Line{spans: [...]}, ...]

Lists, tables & scrolling

List

%List{
  items: ["Item 1", "Item 2", "Item 3"],
  selected: 0,
  highlight_style: %Style{fg: :yellow, modifiers: [:bold]},
  highlight_symbol: ">> ",
  direction: :bottom_to_top,            # default :top_to_bottom (chat logs)
  scroll_padding: 2,                     # default 0; vim's scrolloff
  repeat_highlight_symbol: true,         # default false; multi-row items
  block: %Block{title: " Menu ", borders: [:all]}
}

Table

%Table{
  header: ["Name", "Score"],
  footer: ["Total", "182"],              # optional, renders at bottom
  rows: [["Alice", "95"], ["Bob", "87"]],
  widths: [{:percentage, 70}, {:percentage, 30}],
  selected: 0,                           # row index
  selected_column: 1,                    # required to fire column/cell styles below
  highlight_style: %Style{fg: :yellow},          # row highlight
  header_style: %Style{modifiers: [:bold]},
  footer_style: %Style{fg: :gray},
  column_highlight_style: %Style{bg: :dark_gray},
  cell_highlight_style: %Style{fg: :black, bg: :yellow},
  highlight_spacing: :always,            # :always | :when_selected (default) | :never
  column_spacing: 1,
  block: %Block{borders: [:all]}
}

Without :selected_column set, :column_highlight_style and :cell_highlight_style are no-ops (the activation gate is ratatui's TableState::select_column/1).

WidgetList

%WidgetList{
  items: [
    {%Paragraph{text: "Row 1"}, 3},    # {widget, height}
    {%Paragraph{text: "Row 2"}, 5}
  ],
  selected: 0,
  scroll_offset: 0,
  highlight_style: %Style{bg: {50, 50, 50}}
}

Row-clipped scroll. Good for 1k+ items; only visible rows cross the NIF boundary.

Scrollbar

%Scrollbar{
  orientation: :vertical_right,        # or :vertical_left, :horizontal_bottom, :horizontal_top
  content_length: 100,
  position: 25,
  viewport_content_length: 20,
  thumb_style: %Style{fg: :yellow}
}

Progress & activity

Gauge

%Gauge{
  ratio: 0.65,                         # 0.0..1.0
  label: "65%",
  gauge_style: %Style{fg: :green},
  block: %Block{borders: [:all]}
}

LineGauge

%LineGauge{
  ratio: 0.4,
  label: "Loading...",
  filled_style: %Style{fg: :blue},
  unfilled_style: %Style{fg: :dark_gray}
}

Single-cell-tall bar — great for compact status rows.

Sparkline

%Sparkline{
  data: [0, 1, 3, 5, 8, nil, 2, 4],    # nil = absent
  max: 8,                              # nil auto-scales
  direction: :left_to_right,
  bar_set: :nine_levels,
  style: %Style{fg: :cyan}
}

Throbber

%Throbber{
  label: "Loading",
  throbber_set: :braille,              # :braille | :dots | :line | :ascii
  step: state.tick,                    # caller increments
  throbber_style: %Style{fg: :cyan}
}

Drive with Subscription.interval/3 (reducer) or Process.send_after/3 (callback).

BarChart

alias ExRatatui.Widgets.Bar

%BarChart{
  data: [
    %Bar{label: "Mon", value: 42},
    %Bar{label: "Tue", value: 67, style: %Style{fg: :yellow}}
  ],
  bar_width: 5,
  bar_gap: 1,
  direction: :vertical,                # or :horizontal
  max: 100                             # nil auto-scales
}

Use :groups with %BarGroup{} for side-by-side clusters (mutually exclusive with :data).

Input

TextInput

state = ExRatatui.text_input_new()     # mount once, keep in app state

%TextInput{
  state: state,
  placeholder: "Type here...",
  style: %Style{fg: :white},
  cursor_style: %Style{bg: :white, fg: :black},
  block: %Block{borders: [:all]}
}

ExRatatui.text_input_handle_key(state, key_event)
value = ExRatatui.text_input_get_value(state)

Textarea

state = ExRatatui.textarea_new()

%Textarea{
  state: state,
  placeholder: "Enter message...",
  cursor_line_style: %Style{bg: {40, 40, 40}},
  line_number_style: %Style{fg: :dark_gray},  # nil hides line numbers
  block: %Block{borders: [:all]}
}

ExRatatui.textarea_handle_key(state, key_event)
value = ExRatatui.textarea_get_value(state)

Enter = newline, Ctrl+Enter = submit.

Checkbox

%Checkbox{
  label: "Enable notifications",
  checked: true,
  checked_style: %Style{fg: :green},
  checked_symbol: "[x]",
  unchecked_symbol: "[ ]"
}

SlashCommands

alias ExRatatui.Widgets.SlashCommands
alias ExRatatui.Widgets.SlashCommands.Command

commands = [
  %Command{name: "help", description: "Show help", aliases: ["h", "?"]},
  %Command{name: "quit", description: "Exit", aliases: ["q"]}
]

case SlashCommands.parse(input_text) do
  {:slash, query} -> SlashCommands.match_commands(commands, query)
  :no_slash -> []
end

Pair the matches with a %Popup{} in render/2 to show autocomplete.

Charts & canvas

Chart

alias ExRatatui.Widgets.Chart.{Axis, Dataset}

%Chart{
  datasets: [
    %Dataset{
      name: "CPU",
      data: [{0.0, 12.0}, {1.0, 25.0}, {2.0, 48.0}],
      marker: :braille,
      graph_type: :line,                # :line | :scatter | :bar
      style: %Style{fg: :cyan}
    }
  ],
  x_axis: %Axis{title: "Time", bounds: {0.0, 3.0}, labels: ["0", "1", "2", "3"]},
  y_axis: %Axis{title: "Usage %", bounds: {0.0, 100.0}, labels: ["0", "50", "100"]},
  legend_position: :top_right            # nil hides
}

Canvas

alias ExRatatui.Widgets.Canvas.{Circle, Label, Line, Points, Rectangle}

%Canvas{
  x_bounds: {0.0, 100.0},
  y_bounds: {0.0, 50.0},
  marker: :braille,                     # :braille | :dot | :block | :bar | :half_block
  background_color: :black,
  shapes: [
    %Line{x1: 0.0, y1: 0.0, x2: 100.0, y2: 50.0, color: :cyan},
    %Rectangle{x: 10.0, y: 10.0, width: 30.0, height: 20.0, color: :yellow},
    %Circle{x: 70.0, y: 25.0, radius: 10.0, color: :magenta},
    %Points{coords: [{20.0, 40.0}, {50.0, 30.0}], color: :green},
    %Label{x: 70.0, y: 25.0, text: "★", color: :white}
  ]
}

World map

alias ExRatatui.Widgets.Canvas.Map, as: CanvasMap

%Canvas{
  x_bounds: {-180.0, 180.0},
  y_bounds: {-90.0, 90.0},
  marker: :dot,
  shapes: [
    %CanvasMap{resolution: :high, color: :green},
    %Label{x: -74.0, y: 40.7, text: "NYC", color: :yellow}
  ]
}

Containers & overlays

Block

%Block{
  title: " Panel ",                     # String | Span | Line | [Span]
  borders: [:all],                      # or [:top, :left, ...]
  border_type: :rounded,                # :plain :rounded :double :thick
                                        # :light/heavy_{double,triple,quadruple}_dashed
                                        # :quadrant_inside :quadrant_outside
  border_style: %Style{fg: :cyan},
  padding: {0, 0, 0, 0}                 # {left, right, top, bottom}
}

Wraps another widget via that widget's :block field.

Block multi-title

alias ExRatatui.Widgets.Block.Title

%Block{
  title: "src/lib.rs",                  # top-left, single-title shortcut
  titles: [
    %Title{content: "[3/12]", alignment: :right},
    %Title{content: "modified", position: :bottom, alignment: :left},
    %Title{content: "L42:7",   position: :bottom, alignment: :right}
  ],
  title_position: :top,                 # default for titles without one
  title_alignment: :left,               # default alignment
  title_style: %Style{fg: :gray},       # default style
  borders: [:all]
}

Each %Block.Title{} carries optional :position, :alignment, :style overrides over the block defaults. Raw line-like entries in :titles (e.g. titles: ["status"]) inherit the defaults.

%Popup{
  content: %Paragraph{text: "Are you sure?"},
  percent_width: 50,                    # or fixed_width: 40
  percent_height: 30,                   # or fixed_height: 10
  block: %Block{title: " Confirm ", borders: [:all], border_type: :double}
}

Renders centered on top of whatever's already drawn.

Tabs

%Tabs{
  titles: ["Overview", "Details", "Settings"],
  selected: 0,
  highlight_style: %Style{fg: :yellow, modifiers: [:bold]},
  divider: " | ",
  padding: {1, 1},
  block: %Block{borders: [:bottom]}
}

Clear

%Clear{}

Fills the rect with blank cells. Use before a popup to dim underlying content.

Calendar

%Calendar{
  display_date: ~D[2026-03-15],
  events: [
    {~D[2026-03-10], %Style{fg: :red, modifiers: [:bold]}},
    {~D[2026-03-20], %Style{fg: :green}}
  ],
  default_style: %Style{fg: :white},
  show_month_header: true,
  header_style: %Style{fg: :yellow, modifiers: [:bold]},
  show_weekdays_header: true,
  weekday_style: %Style{fg: :cyan},
  show_surrounding: %Style{fg: :dark_gray},   # nil hides surrounding days
  block: %Block{title: " March ", borders: [:all]}
}

Needs ~22×8 cells (24×10 with a block). events accepts a keyword-like list of {%Date{}, %Style{}} tuples or a %{Date => Style} map.

Image

{:ok, picture} = ExRatatui.Image.new(File.read!("priv/cover.png"),
  protocol: :auto,        # or :halfblocks / :kitty / :sixel / :iterm2
  resize: :fit,           # or :crop / :scale
  background: {0, 0, 0}   # nil for transparent
)

# Drop into a render tree like any other widget:
[{picture, area}]

Hold the handle in your model (decode is non-trivial — don't rebuild per frame). :auto adapts to the transport: Kitty/Ghostty get native graphics, Livebook gets halfblocks. Errors return {:error, {:decode_failed, msg}}.

Probe the local terminal for best protocol detection:

ExRatatui.Image.auto_local_protocol(terminal_ref)

Over SSH or Distributed, declare both the protocol and the client's cell pixel size (needed for accurate Kitty/Sixel/iTerm2 scaling):

ExRatatui.SSH.Daemon.start_link(
  mod: MyApp.TUI,
  image_protocol: :kitty,
  image_font_size: {10, 20}
)

ExRatatui.Distributed.attach(:"app@host", MyApp.TUI,
  image_protocol: :kitty,
  image_font_size: {10, 20}
)

See Images for the full transport / protocol resolution table.