This guide covers the building blocks for constructing screens in render/2: widgets, layout, styles, and events. Everything here works identically in both the Callback Runtime and Reducer Runtime.

Layout

render/2 returns a list of {widget, rect} tuples. Each %ExRatatui.Layout.Rect{} defines a rectangular area on the screen. Use ExRatatui.Layout.split/3 to divide areas into sub-regions using constraints:

alias ExRatatui.Layout
alias ExRatatui.Layout.Rect

area = %Rect{x: 0, y: 0, width: 80, height: 24}

# Three-row layout: header, body, footer
[header, body, footer] = Layout.split(area, :vertical, [
  {:length, 3},
  {:min, 0},
  {:length, 1}
])

# Split body into sidebar + main
[sidebar, main] = Layout.split(body, :horizontal, [
  {:percentage, 30},
  {:percentage, 70}
])

Constraint Types

ConstraintDescription
{:percentage, n}Percentage of the available space (0–100)
{:length, n}Exact number of rows or columns
{:min, n}At least n rows/columns, expands to fill remaining space
{:max, n}At most n rows/columns
{:ratio, num, den}Fraction of available space (e.g., {:ratio, 1, 3} for one-third)

Styles

Styles control foreground color, background color, and text modifiers:

alias ExRatatui.Style

# Named colors
%Style{fg: :green, bg: :black}

# RGB
%Style{fg: {:rgb, 255, 100, 0}}

# 256-color indexed
%Style{fg: {:indexed, 42}}

# Modifiers
%Style{modifiers: [:bold, :dim, :italic, :underlined, :crossed_out, :reversed]}

Named colors: :black, :red, :green, :yellow, :blue, :magenta, :cyan, :gray, :dark_gray, :light_red, :light_green, :light_yellow, :light_blue, :light_magenta, :light_cyan, :white, :reset.

Styles can be applied to most widgets via the :style field, and many widgets accept additional style fields for specific parts (e.g., highlight_style, border_style).

Events

Terminal events are polled automatically by the runtime. In the Callback Runtime, they arrive in handle_event/2. In the Reducer Runtime, they arrive as {:event, event} in update/2.

Key Events

%ExRatatui.Event.Key{
  code: "q",          # key name: "a"-"z", "up", "down", "enter", "esc", "tab", etc.
  kind: "press",      # "press", "release", or "repeat"
  modifiers: []       # list of "ctrl", "alt", "shift", "super", "hyper", "meta"
}

Mouse Events

%ExRatatui.Event.Mouse{
  kind: "down",       # "down", "up", "drag", "moved", "scroll_down", "scroll_up"
  column: 10,
  row: 5,
  modifiers: []
}

Resize Events

%ExRatatui.Event.Resize{
  width: 120,
  height: 40
}

The runtime automatically re-renders on resize — you don't need to handle resize events unless your app needs to react to size changes in its state.

Widgets

Paragraph

Displays text with support for alignment, wrapping, and scrolling.

%Paragraph{
  text: "Hello, world!\nSecond line.",
  style: %Style{fg: :cyan, modifiers: [:bold]},
  alignment: :center,
  wrap: true
}

Block

A container with borders and a title. Any widget can be wrapped inside a Block via its :block field.

%Block{
  title: "My Panel",
  borders: [:all],
  border_type: :rounded,
  border_style: %Style{fg: :blue}
}

# Compose with other widgets:
%Paragraph{
  text: "Inside a box",
  block: %Block{title: "Title", borders: [:all]}
}

Border types: :plain, :rounded, :double, :thick.

List

A selectable list with highlight support for the current item.

%List{
  items: ["Elixir", "Rust", "Haskell"],
  highlight_style: %Style{fg: :yellow, modifiers: [:bold]},
  highlight_symbol: " > ",
  selected: 0,
  block: %Block{title: " Languages ", borders: [:all]}
}

Table

A table with headers, rows, and column width constraints.

%Table{
  rows: [["Alice", "30"], ["Bob", "25"]],
  header: ["Name", "Age"],
  widths: [{:length, 15}, {:length, 10}],
  highlight_style: %Style{fg: :yellow},
  selected: 0
}

Gauge

A progress bar that fills proportionally to a given ratio.

%Gauge{
  ratio: 0.75,
  label: "75%",
  gauge_style: %Style{fg: :green}
}

LineGauge

A thin single-line progress bar using line-drawing characters, with separate styles for the filled and unfilled portions.

%LineGauge{
  ratio: 0.6,
  label: "60%",
  filled_style: %Style{fg: :green},
  unfilled_style: %Style{fg: :dark_gray}
}

Tabs

A tab bar for switching between views.

%Tabs{
  titles: ["Home", "Settings", "Help"],
  selected: 0,
  highlight_style: %Style{fg: :cyan, modifiers: [:bold]},
  divider: " | ",
  block: %Block{borders: [:all]}
}

Scrollbar

A scroll position indicator for long content, supporting both vertical and horizontal orientations.

%Scrollbar{
  content_length: 100,
  position: 25,
  viewport_content_length: 10,
  orientation: :vertical_right,
  thumb_style: %Style{fg: :cyan}
}

Orientations: :vertical_right, :vertical_left, :horizontal_bottom, :horizontal_top.

Checkbox

A boolean toggle with customizable checked and unchecked symbols.

%Checkbox{
  label: "Enable notifications",
  checked: true,
  checked_style: %Style{fg: :green},
  checked_symbol: "✓",
  unchecked_symbol: "✗"
}

TextInput

A single-line text input with cursor navigation and viewport scrolling. This is a stateful widget — its state lives in Rust via ResourceArc.

# Create state (once, e.g. in mount/1 or init/1)
state = ExRatatui.text_input_new()

# Forward key events
ExRatatui.text_input_handle_key(state, "h")
ExRatatui.text_input_handle_key(state, "i")

# Read/set value
ExRatatui.text_input_get_value(state)  #=> "hi"
ExRatatui.text_input_set_value(state, "hello")

# Render
%TextInput{
  state: state,
  style: %Style{fg: :white},
  cursor_style: %Style{fg: :black, bg: :white},
  placeholder: "Type here...",
  placeholder_style: %Style{fg: :dark_gray},
  block: %Block{title: "Search", borders: [:all], border_type: :rounded}
}

Clear

Resets all cells in its area to empty space characters. This is useful for clearing a region before rendering an overlay on top of existing content.

%Clear{}

Markdown

Renders markdown text with syntax-highlighted code blocks, powered by tui-markdown (pulldown-cmark + syntect). Supports headings, bold, italic, inline code, fenced code blocks, bullet lists, links, and horizontal rules.

%Markdown{
  content: "# Hello\n\nSome **bold** text and `inline code`.\n\n```elixir\nIO.puts(\"hi\")\n```",
  wrap: true,
  block: %Block{title: "Response", borders: [:all]}
}

Textarea

A multiline text editor with undo/redo, cursor movement, and Emacs-style shortcuts. This is a stateful widget — its state lives in Rust via ResourceArc.

# Create state (once, e.g. in mount/1 or init/1)
state = ExRatatui.textarea_new()

# Forward key events (with modifier support)
ExRatatui.textarea_handle_key(state, "h", [])
ExRatatui.textarea_handle_key(state, "enter", [])
ExRatatui.textarea_handle_key(state, "w", ["ctrl"])  # delete word backward

# Read value
ExRatatui.textarea_get_value(state)  #=> "h\n"

# Render
%Textarea{
  state: state,
  placeholder: "Type your message...",
  placeholder_style: %Style{fg: :dark_gray},
  block: %Block{title: "Message", borders: [:all], border_type: :rounded}
}

Throbber

A loading spinner that animates through symbol sets. The caller controls the animation by incrementing :step on each tick.

%Throbber{
  label: "Loading...",
  step: state.tick,
  throbber_set: :braille,
  throbber_style: %Style{fg: :cyan},
  block: %Block{title: "Status", borders: [:all]}
}

Available sets: :braille, :dots, :ascii, :vertical_block, :horizontal_block, :arrow, :clock, :box_drawing, :quadrant_block, :white_square, :white_circle, :black_circle.

A centered modal overlay that renders any widget over the parent area, clearing the background underneath. Useful for dialogs, confirmations, and command palettes.

%Popup{
  content: %Paragraph{text: "Are you sure?"},
  block: %Block{title: "Confirm", borders: [:all], border_type: :rounded},
  percent_width: 50,
  percent_height: 30
}

WidgetList

A vertical list of heterogeneous widgets with optional selection and scrolling. Each item is a {widget, height} tuple, making it ideal for chat message histories and similar layouts where items have different heights.

scroll_offset is a row offset from the top of the content, not an item index. To scroll to a specific item, sum the heights of all preceding items. Items partially above the viewport are clipped row-by-row instead of being dropped entirely.

%WidgetList{
  items: [
    {%Paragraph{text: "User: Hello!"}, 1},
    {%Markdown{content: "**Bot:** Hi there!\n\nHow can I help?"}, 4},
    {%Paragraph{text: "User: What is Elixir?"}, 1}
  ],
  selected: 1,
  highlight_style: %Style{fg: :yellow},
  scroll_offset: 0,
  block: %Block{title: "Chat", borders: [:all]}
}

SlashCommands

A command palette with fuzzy search that renders a scrollable list of SlashCommands.Command structs filtered by the current input.

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

%SlashCommands{
  commands: [
    %Command{name: "help", description: "Show help"},
    %Command{name: "quit", description: "Exit the app"}
  ],
  input: "he",
  selected: 0,
  block: %Block{title: "Commands", borders: [:all]}
}

Examples