TUIs with ratatui
Build interactive terminal UIs with ratatui, the maintained fork of tui-rs.
Recipe
use crossterm::event::{self, Event, KeyCode};
use ratatui::{prelude::*, widgets::Paragraph};
fn main() -> anyhow::Result<()> {
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
terminal.clear()?;
loop {
terminal.draw(|f| {
let area = f.area();
f.render_widget(Paragraph::new("Press q to quit"), area);
})?;
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') { break; }
}
}
terminal.show_cursor()?;
Ok(())
}When to reach for this: Dashboards, log viewers, or wizards where line-based CLIs are insufficient.
Working Example
use crossterm::event::{self, Event, KeyCode};
use ratatui::{
layout::{Constraint, Direction, Layout},
prelude::*,
widgets::{Block, Borders, List, ListItem},
};
struct App {
items: Vec<String>,
selected: usize,
}
impl App {
fn next(&mut self) {
self.selected = (self.selected + 1) % self.items.len();
}
}
fn main() -> anyhow::Result<()> {
let mut app = App {
items: vec!["alpha".into(), "beta".into(), "gamma".into()],
selected: 0,
};
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
loop {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(3), Constraint::Length(1)])
.split(f.area());
let items: Vec<ListItem> = app.items.iter()
.enumerate()
.map(|(i, s)| {
let style = if i == app.selected {
Style::default().bg(Color::Blue)
} else {
Style::default()
};
ListItem::new(s.as_str()).style(style)
})
.collect();
f.render_widget(
List::new(items).block(Block::default().borders(Borders::ALL).title("Items")),
chunks[0],
);
})?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Down | KeyCode::Char('j') => app.next(),
_ => {}
}
}
}
Ok(())
}What this demonstrates:
- Layout splits the screen into regions
- List widget with selection highlighting
- Event loop reads keyboard input via
crossterm - Renders to stderr to keep stdout free
Deep Dive
Architecture
- Backend -
CrosstermBackendhandles terminal raw mode - Widget tree - Compose
Paragraph,Table,Chart,Tabs - Event loop - Poll or blocking
event::read()each frame - State - Keep app state in a struct;
drawis pure rendering
Common Widgets
| Widget | Use for |
|---|---|
List | Selectable menus |
Table | Tabular data |
Block | Titled panels with borders |
Gauge | Progress visualization |
Gotchas
- Forgot raw mode cleanup - Terminal stays broken after panic. Fix: Use
ratatui::init()/restore()or aDropguard. - Rendering to stdout - Breaks piping and mixes with data output. Fix: Use stderr as the backend stream.
- Busy-wait loops - 100% CPU without
polltimeout. Fix: Useevent::poll(Duration::from_millis(100)). - Resizing - Layout breaks on terminal resize. Fix: Handle
Event::Resizeand redraw. - Mouse support - Not enabled by default. Fix: Enable crossterm mouse capture when needed.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Line-based CLI + inquire | Simple prompts | Multi-panel dashboards |
dialoguer | Yes/no and select prompts | Complex layouts |
| Web UI | Rich graphics needed | SSH-only server environments |
FAQs
ratatui vs tui-rs?
ratatui is the actively maintained fork. New projects should use ratatui.
Can I use tokio with ratatui?
Yes. Run the event loop on a blocking thread or use tokio::select! with async channels for updates.
How do I test TUIs?
Extract rendering into functions taking Buffer and assert on cell content.
Charts and graphs?
ratatui includes Chart for sparklines and line charts. For heavy data, consider exporting to a browser.
Multiple screens?
Use an enum for app state (List, Detail, Help) and match in draw.
Color themes?
Define a Theme struct with Style values. Support light/dark via flag.
Clipboard support?
Integrate arboard for copy/paste outside the basic TUI scope.
Windows support?
crossterm supports Windows terminals. Test on your target terminal emulator.
Embedding in existing CLI?
Launch TUI subcommand: myapp tui enters full-screen; default mode stays line-based.
Accessibility?
Provide equivalent non-TUI commands (--plain, --json) for screen readers and scripting.
Related
- Terminal Output - progress and color
- CLI Basics - when not to use a TUI
- Error Reporting - errors outside the TUI loop
- CLI Best Practices - always offer non-interactive mode
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.