Ratatui: Testing & Ecosystem

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

A full-screen program is hard to test by hand: it takes over the terminal, it redraws constantly and its output is a grid of styled cells, not a stream of lines. Ratatui makes that easier than most libraries, because every widget renders into an in-memory Buffer and the library ships a TestBackend that keeps the final buffer in memory instead of writing escape sequences. This page covers the rest of what a Ratatui project needs once the screen works: testing the rendering, the keyboard handling and the resize behaviour; recording demonstrations; choosing third-party widgets and understanding their maintenance status; deciding between Ratatui alone and a framework layer such as tui-realm; running Ratatui where there is no ordinary terminal; and upgrading a library that has not reached 1.0.

The page was written against Ratatui 0.30.2 (with ratatui-core 0.1.2), crossterm 0.29.0, insta 1.49.0 and cargo-insta 1.49.0, ratatui-textarea 0.9.3, tui-input 0.15.5, tui-tree-widget 0.24.1, tui-big-text 0.8.10, ratatui-image 11.1.0, tui-realm 4.1.0 and VHS v0.12.1, as listed on crates.io, docs.rs and GitHub on 2026-10-25. The platforms are Windows, macOS and Linux; the test code in the first half is platform-independent and runs the same everywhere cargo test runs. Neither cargo nor rustc is installed on the machine used to write this page, so the Rust examples were not compiled or run: every type, method and macro in them was checked against the documentation on docs.rs, but compile each snippet before relying on it, and expect to adjust the expected strings of the snapshot and buffer tests to whatever your first real run prints. The application itself (state, layout, widgets, the event loop) is built on Ratatui: Getting Started, Ratatui: Layout, Widgets & Styling and Ratatui: Events, Async & Application Architecture; the concepts shared by all TUI libraries are on TUI Architecture & Concepts; testing through a real pseudo-terminal, for every language, is on Testing Terminal Apps & TUIs. The design rules for command-line tests (exit codes, streams, golden output) are on Testing CLIs.

Ratatui has no 1.0

Before anything else: Ratatui is a 0.x library. The latest release on crates.io is 0.30.2 (published 2026-06-19), and neither the crate documentation nor the website announces a 1.0 release or a roadmap towards one. In Cargo, that has concrete consequences.

  • Under the semver rules Cargo applies, a 0.x minor release is allowed to break the API. a dependency on ratatui with the requirement 0.30 means at least 0.30.0 and below 0.31.0, so Cargo will not move you to 0.31 by itself, and moving there is a deliberate edit.

  • Ratatui keeps a manually compiled BREAKING-CHANGES.md in its repository, with one section per version, and labels the pull requests that break the API. At the time of writing, the file on the main branch already has a section for an unreleased 0.31.0 (the Backend trait gains cursor save and restore methods), which shows that the next minor version will break something again.

  • The contribution guide asks maintainers to deprecate an item before removing it, to wait at least two versions before removing deprecated items, and to put breaking changes in BREAKING-CHANGES.md. That is a project practice, not a stability guarantee: it does not promise how many releases there will be, or when.

  • The same guide says the code must compile on stable Rust and on the minimum supported Rust version (MSRV). The MSRV itself moves between minor versions; the breaking-changes file records 1.86.0 for 0.30.0 and 1.88.0 from 0.30.1.

  • The crate split in 0.30 was introduced partly "to provide better API stability for third-party widget libraries": ratatui-core holds the fundamental types and traits and changes more slowly than the main crate. Widget authors can depend on it; application authors can keep depending on ratatui.

The practical rule is to treat every Ratatui upgrade as a small migration, to pin the minor version, and to have tests that fail loudly when rendering changes. The rest of this page is mostly about the second half of that sentence, and a section at the end covers the first.

Testing the rendering

The pieces

A Ratatui test needs no terminal at all. Four types from the library are involved.

Type Role in a test

Buffer

A rectangle of Cell values. Every widget draws into one. It implements PartialEq, so two buffers can be compared, and a readable Debug that prints the area, the rows as strings and style information only where it changes.

Cell

One screen position: a grapheme (symbol()), public fg, bg and modifier fields, and style() to read them back as a Style.

TestBackend

A Backend that stores the buffer in memory. TestBackend::new(width, height) creates an empty one; TestBackend::with_lines(lines) creates one whose initial screen and size come from the given lines.

Terminal

The same Terminal the application uses, constructed over the TestBackend, so that terminal.draw(...) exercises the real double-buffer and diff code path.

Since 0.30 the TestBackend uses core::convert::Infallible as its error type instead of std::io::Error, one of the changes listed for that release, so Terminal::new(TestBackend::new(w, h)) and terminal.draw(...) cannot fail in a test and .unwrap() is always safe there.

The TestBackend and Buffer API

Method Purpose

TestBackend::new(width, height)

Empty screen of the given size.

TestBackend::with_lines(lines)

Screen pre-filled from lines; the size is taken from them.

buffer()

Borrow the current buffer for custom assertions or cell-level inspection.

assert_buffer(&expected)

Panics with a diff if the screen differs from a Buffer, styles included.

assert_buffer_lines(lines)

Panics with a diff if the screen differs from the given lines. Compares symbols and also styles, so it is meant for unstyled expectations; use assert_buffer when styles matter.

resize(width, height)

Changes the size of the virtual screen. The next Terminal::draw notices and re-lays-out, which is how a resize is simulated.

cursor_position(), cursor_visible(), assert_cursor_position(position)

Inspect or assert where the application left the cursor, which matters for text inputs.

scrollback(), assert_scrollback(&expected), assert_scrollback_lines(lines), assert_scrollback_empty()

Inspect what has been pushed above an inline viewport (see the inline viewport section of Ratatui: Events, Async & Application Architecture).

Display

Formatting the backend with the standard display placeholder renders the buffer as quoted rows of text; this is what snapshot tests record.

On the Buffer side the useful calls in tests are Buffer::with_lines(lines) and Buffer::empty(area) to build an expectation, set_string(x, y, text, style) and set_style(area, style) to paint the expectation, cell(position) for a checked lookup that returns an Option, and indexing with buffer[(x, y)], which panics outside the area. Buffer::diff returns the cells that differ between two buffers, which is what the real backend sends to the terminal; it panics if the two buffers do not have the same area.

A testable catalogue screen

The shelf TUI is a catalogue browser. For the tests below it is reduced to its smallest testable shape: a state struct, a pure function for key handling, and a Widget implementation for &Catalogue, so that nothing in it needs a real terminal. The same split (state, handle_key, render into a Buffer) is the one recommended by the Elm-style architecture described on the architecture page.

# Cargo.toml
[package]
name = "shelf"
version = "0.1.0"
edition = "2024"

[dependencies]
ratatui = "0.30"

[dev-dependencies]
insta = "1.49"
// src/catalogue.rs
use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::widgets::{Block, List, ListItem, Widget};

pub struct Book {
    pub title: &'static str,
    pub on_loan: bool,
}

pub struct Catalogue {
    pub books: Vec<Book>,
    pub selected: usize,
    pub quit: bool,
}

impl Catalogue {
    pub fn new(books: Vec<Book>) -> Self {
        Self { books, selected: 0, quit: false }
    }

    /// Pure state transition: no terminal, no I/O.
    pub fn handle_key(&mut self, key: KeyEvent) {
        // Windows reports releases too; see the Ratatui FAQ.
        if key.kind != KeyEventKind::Press {
            return;
        }
        match key.code {
            KeyCode::Down | KeyCode::Char('j') => {
                if self.selected + 1 < self.books.len() {
                    self.selected += 1;
                }
            }
            KeyCode::Up | KeyCode::Char('k') => {
                self.selected = self.selected.saturating_sub(1);
            }
            KeyCode::Char('q') | KeyCode::Esc => self.quit = true,
            _ => {}
        }
    }
}

impl Widget for &Catalogue {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let block = Block::bordered().title("Shelf");
        let inner = block.inner(area);
        block.render(area, buf);

        let rows = self.books.iter().enumerate().map(|(index, book)| {
            let marker = if index == self.selected { ">" } else { " " };
            let loan = if book.on_loan { " [on loan]" } else { "" };
            let item = ListItem::new(format!("{marker} {}{loan}", book.title));
            if index == self.selected {
                item.style(Style::new().add_modifier(Modifier::REVERSED))
            } else {
                item
            }
        });
        List::new(rows).render(inner, buf);
    }
}

Asserting on the screen

The tests live in the same file. Each one builds the state, draws it through a real Terminal over a TestBackend, and asserts on the backend.

// src/catalogue.rs (continued)
#[cfg(test)]
mod tests {
    use super::*;
    use ratatui::Terminal;
    use ratatui::backend::TestBackend;
    use ratatui::crossterm::event::KeyModifiers;

    fn sample() -> Catalogue {
        Catalogue::new(vec![
            Book { title: "Dune", on_loan: false },
            Book { title: "Neuromancer", on_loan: true },
            Book { title: "Hyperion", on_loan: false },
        ])
    }

    fn draw(app: &Catalogue, width: u16, height: u16) -> Terminal<TestBackend> {
        let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
        terminal.draw(|frame| frame.render_widget(app, frame.area())).unwrap();
        terminal
    }

    #[test]
    fn renders_the_three_books() {
        let terminal = draw(&sample(), 26, 5);
        terminal.backend().assert_buffer_lines([
            "┌Shelf───────────────────┐",
            "│> Dune                  │",
            "│  Neuromancer [on loan] │",
            "│  Hyperion              │",
            "└────────────────────────┘",
        ]);
    }
}

assert_buffer_lines is the fastest way to pin down layout: the expected value is a picture of the screen, a mismatch panics with a row-by-row diff, and nothing is written to disk. Its weakness is that any border style or character you did not intend (Ratatui lets a Block use plain, rounded, double or thick borders) shows up as a test failure, which is usually exactly what you want from a rendering test.

Colours and modifiers need a different assertion. The selected row is rendered with the reversed modifier, which the text rows above cannot show. A full Buffer expectation compares styles too:

    #[test]
    fn the_selected_row_is_reversed() {
        let mut app = sample();
        app.handle_key(KeyEvent::from(KeyCode::Down));
        let terminal = draw(&app, 26, 5);

        let mut expected = Buffer::with_lines([
            "┌Shelf───────────────────┐",
            "│  Dune                  │",
            "│> Neuromancer [on loan] │",
            "│  Hyperion              │",
            "└────────────────────────┘",
        ]);
        expected.set_style(
            Rect::new(1, 2, 24, 1),
            Style::new().add_modifier(Modifier::REVERSED),
        );
        terminal.backend().assert_buffer(&expected);
    }

    #[test]
    fn only_the_selected_row_is_reversed() {
        let mut app = sample();
        app.handle_key(KeyEvent::from(KeyCode::Down));
        let terminal = draw(&app, 26, 5);
        let buffer = terminal.backend().buffer();

        assert!(buffer[(1, 2)].modifier.contains(Modifier::REVERSED));
        assert!(!buffer[(1, 1)].modifier.contains(Modifier::REVERSED));
        assert!(!buffer[(1, 3)].modifier.contains(Modifier::REVERSED));
    }

Rect::new(1, 2, 24, 1) is the inside of the border (x 1, y 2, width 24, height 1): the second row of the list. The second test shows the cell-level alternative: when only one property matters, reading buffer[(x, y)] produces a failure message about that property rather than a screen diff.

Testing input and resize

Because handle_key is a plain method, the keyboard is tested without drawing anything. Constructing a KeyEvent needs no terminal either: KeyEvent::from(KeyCode::Down) builds a press, and KeyEvent::new_with_kind builds anything else. That is how the Windows pitfall is pinned down by a test. On Windows, crossterm reports both the press and the release of a key, while on macOS and Linux it reports presses only unless the keyboard-enhancement flags are enabled, so a handler that ignores KeyEvent::kind moves the selection twice per keystroke on one platform and once on the others. The test below fails on every platform if the guard is removed, instead of only on a Windows machine.

    #[test]
    fn navigation_clamps_at_both_ends() {
        let mut app = sample();
        app.handle_key(KeyEvent::from(KeyCode::Up));
        assert_eq!(app.selected, 0);
        for _ in 0..5 {
            app.handle_key(KeyEvent::from(KeyCode::Char('j')));
        }
        assert_eq!(app.selected, 2);
    }

    #[test]
    fn key_releases_are_ignored() {
        let mut app = sample();
        let release = KeyEvent::new_with_kind(
            KeyCode::Down,
            KeyModifiers::NONE,
            KeyEventKind::Release,
        );
        app.handle_key(release);
        assert_eq!(app.selected, 0);
    }

    #[test]
    fn q_quits() {
        let mut app = sample();
        app.handle_key(KeyEvent::from(KeyCode::Char('q')));
        assert!(app.quit);
    }

    #[test]
    fn a_narrow_terminal_truncates_long_titles() {
        let app = sample();
        let mut terminal = draw(&app, 26, 5);

        terminal.backend_mut().resize(14, 5);
        terminal.draw(|frame| frame.render_widget(&app, frame.area())).unwrap();

        terminal.backend().assert_buffer_lines([
            "┌Shelf───────┐",
            "│> Dune      │",
            "│  Neuromance│",
            "│  Hyperion  │",
            "└────────────┘",
        ]);
    }
}

The last test simulates a resize the way the real program experiences one: the backend changes size, the next draw call detects it, and the widgets lay themselves out in the new area. A test of the real event path (the Event::Resize that crossterm delivers, or the signal that triggers it) needs a real pseudo-terminal and belongs to the end-to-end layer described below.

Snapshot tests with insta

Hand-written expected screens stop being practical once the screen is large. The Ratatui website documents a snapshot workflow with the insta crate and its cargo-insta companion: the first run records the rendered screen, later runs compare against it, and an intended change is accepted in a review step.

cargo install cargo-insta
cargo add insta --dev
    #[test]
    fn catalogue_snapshot() {
        let terminal = draw(&sample(), 40, 8);
        insta::assert_snapshot!(terminal.backend());
    }

assert_snapshot! takes anything that implements Display, and TestBackend does: it prints the buffer as one quoted string per row. The first run writes a file under snapshots/ next to the source file, named after the crate, the module path and the test, and the documented shape of such a file is a small header followed by the rows:

---
source: src/catalogue.rs
expression: terminal.backend()
---
"┌Shelf─────────────────────────────────┐"
"│> Dune                                │"
"│  Neuromancer [on loan]               │"
"│  Hyperion                            │"
"│                                      │"
"│                                      │"
"│                                      │"
"└──────────────────────────────────────┘"

The workflow around it is the one insta documents for all Rust code.

Command or variable Effect

cargo test

Compares each snapshot. A new or changed snapshot makes the test fail and, outside CI, writes the new value next to the old one with a .snap.new extension.

cargo insta review

Interactive review: accept, reject or skip each pending snapshot.

cargo insta test

Runs the tests and collects the pending changes from all assertions in one pass; cargo insta test --review adds the review step.

INSTA_UPDATE

Chooses what happens on a mismatch: auto (the default; behaves as no in CI and as new elsewhere), new (write .snap.new files), always (overwrite .snap files without review), unseen (overwrite only new snapshots), no (never write) and force (rewrite even passing snapshots).

CI=true

Insta detects CI through this variable and does not write snapshot files there, so a changed screen fails the build instead of silently updating.

Commit the .snap files, review them in pull requests like any other code, and never commit .snap.new files.

What the layers can and cannot see

Layer Sees Does not see

assert_buffer_lines and cell assertions

Symbols, geometry, and (with assert_buffer or cell access) colours and modifiers

Escape sequences, real terminal behaviour

insta snapshot of TestBackend

The symbols of every row. The Ratatui page states that asserting colour in these snapshots is not supported (it links an open upstream issue), so the snapshot records text only

Colours, modifiers, the cursor position

State tests on handle_key and update

Every transition, deterministically and fast

Whether the real input source produces those events

Pseudo-terminal end-to-end tests

The compiled binary: raw mode, the alternate screen, exit codes, real resize

Nothing is mocked, but they are slow and timing-sensitive

Two further facts follow from how TestBackend works. It never writes escape sequences, so a bug in how a backend translates cells into SGR codes or cursor moves is invisible to it; those bugs belong to the backend crates and are platform-specific (the same cell can be coloured differently by the legacy Windows console and by a VT terminal, see Windows Console & Processes and Escape Sequences & Terminal Capabilities). And it has a fixed width and height, so a snapshot says nothing about the sizes you did not test: pick two or three (a minimum, a typical 80x24 and a wide one) and test each.

Whole-application tests through a pseudo-terminal

TestBackend tests rendering in-process. They do not cover the event loop, terminal setup and teardown, panic hooks or exit codes. The Ratatui snapshot recipe points at running the compiled binary in a pseudo-terminal, sending it keystrokes and asserting on the rendered screen, and names one crate for it, termlens (0.11.6 on crates.io, a young crate with a small number of downloads at the time of writing, so evaluate it before committing to it). The recipe’s example spawns the binary through the CARGO_BIN_EXE_<name> variable that Cargo sets for integration tests, waits for visible text instead of sleeping, and ends with wait_exit and an assertion that the program succeeded. Real-PTY testing for every language, including the Windows ConPTY case and the tools (expect, pexpect, rexpect, tmux), is the subject of Testing Terminal Apps & TUIs; the terminal model underneath is on Terminals, TTYs & Pseudo-Terminals.

Debugging while the library owns the terminal

A running Ratatui program owns the screen, so println! and dbg! corrupt it. The documentation suggests two techniques: write logs to a file, or use the tui-logger widget crate (0.18.3), which captures log records and shows them inside the TUI; and for widget state, render the Debug text of the state in a panel that a key toggles on and off, which the recipe shows with a show_debug field on the application.

Recording demonstrations with VHS

A screenshot of a TUI goes stale as fast as the code changes. VHS (from Charm, v0.12.1) turns a text script, a tape, into a GIF, MP4, WebM or PNG sequence by driving a real terminal, so the same tape regenerates the demo whenever the UI changes. The Ratatui project itself keeps .tape files for the recipes in its website repository. VHS is written in Go and is independent of the language of the program it records.

# docs/shelf-tui.tape
Output docs/shelf-tui.gif
Output docs/shelf-tui.txt

Require shelf

Set Shell "bash"
Set FontSize 20
Set Width 1000
Set Height 500
Set TypingSpeed 80ms

Type "shelf tui"
Enter
Sleep 1s
Down 2
Sleep 500ms
Type "q"
Sleep 500ms
vhs docs/shelf-tui.tape

The commands used above are documented in the VHS reference: Output names each artifact (several can be listed, and a .txt or .ascii output records the final text screen), Require fails early when a program is not on the PATH and must appear before any other command, Set configures the shell, font, size and typing speed, Type and Enter send input, Down 2 presses an arrow key twice, and Sleep waits. Besides demos, the README describes the text output as a way to keep golden files in version control and notice output changes between runs.

Platform Installing VHS

macOS and Linux

brew install vhs; packages for Arch, Nix, and Debian-style and RPM-style distributions through Charm’s repositories.

Windows

winget or Scoop. Outside Docker, VHS needs ttyd and ffmpeg on the PATH.

Any

go install github.com/charmbracelet/vhs@latest, or the ghcr.io/charmbracelet/vhs Docker image, which bundles the dependencies.

Pitfalls: a recording that relies on timing (Sleep) is only as deterministic as the program is, so make the program start in a known state (a fixed data file, no clock on screen); recordings render in VHS’s own terminal and font, so they show how the program looks there and not in the reader’s terminal; and vhs publish uploads a GIF to a public server, which is not something to do with a demo that shows private data.

Third-party widgets and crates

Ratatui itself ships the widgets described on Ratatui: Layout, Widgets & Styling. The Ratatui website keeps a Third Party Widgets showcase for community crates and an ecosystem section for larger pieces. Two cautions apply to all of them: they are maintained by other people, on their own schedules, and a crate is only usable with a given Ratatui if its dependency requirement overlaps. Since 0.30, widget crates can depend on ratatui-core and ratatui-widgets instead of ratatui, which makes them less likely to break on every Ratatui release.

The table lists the crates mentioned in the showcase and relevant to the shelf TUI. Versions and release dates are the ones crates.io reported on 2026-10-25; the "Ratatui requirement" column is read from each crate’s published dependencies.

Crate Latest release Ratatui requirement What it is and its status

ratatui-textarea

0.9.3 (2026-10-05)

ratatui-core 0.1, ratatui-widgets 0.3

Multi-line text editor widget, a fork of tui-textarea published from the Ratatui organisation. Listed in the showcase. Not 1.0.

tui-textarea

0.7.0 (2024-10-22)

ratatui 0.29 (optionally tui 0.19)

The original editor widget by rhysd. Its last release on crates.io is from October 2024, so its published dependencies stop at Ratatui 0.29 (it is described here as dormant for that reason, from the release history alone). Use ratatui-textarea for new code.

tui-input

0.15.5 (2026-09-26)

ratatui 0.30.2 (optional)

Single-line input buffer with cursor handling and several backends (crossterm, termion). Not listed in the showcase page, and fits a search box.

tui-tree-widget

0.24.1 (2026-08-09)

ratatui-core 0.1, ratatui-widgets 0.3

Tree view with open and closed nodes and a TreeState that stores selection. Listed in the showcase.

tui-big-text

0.8.10 (2026-09-24)

ratatui-core 0.1, ratatui-widgets 0.3

Large pixel text from the font8x8 glyphs, with sizes from full cell to octant. Part of the tui-widgets repository in the Ratatui organisation. Listed in the showcase.

ratatui-image

11.1.0 stable (2026-10-18; a 12.0.0 release candidate is also published)

ratatui 0.30.1

Image widget with Sixel, Kitty and iTerm2 protocols and a Unicode half-block fallback. Listed in the showcase. It queries the terminal, so see the pitfalls below.

tui-logger

0.18.3 (2026-07-04)

ratatui 0.30

Log capture and display widget, recommended by the debugging recipe and listed in the showcase.

tui-scrollview

0.6.8 (2026-09-24)

ratatui-core 0.1, ratatui-widgets 0.3

Scrollable view over content larger than the screen. Listed in the showcase.

tui-term

0.3.4 (2026-04-07)

ratatui-core 0.1, ratatui-widgets 0.3

A pseudo-terminal widget, for embedding another program’s output in a pane. Listed in the showcase.

throbber-widgets-tui

0.11.1 (2026-06-19)

ratatui 0.30

Spinner and activity indicator widget. Listed in the showcase.

Pitfalls common to this list:

  • Before adding a dependency, run cargo tree -d to see whether it pulls in a second Ratatui or a second crossterm major version. Different crossterm majors keep separate event queues and separate raw-mode state, cannot exchange types even when the names match, and can leave the terminal in raw mode on exit; the symptoms are confusing type errors and lost events. Ratatui’s backends page describes the ratatui-crossterm package, which selects a crossterm major version through crossterm_{version} feature flags, as the mitigation; the main package uses crossterm 0.29 by default.

  • Check the date of the last release and the Ratatui version it depends on. A crate that last released against 0.2x will pull in an old Ratatui next to the one the application uses.

  • Treat every one of these crates as 0.x: their own APIs change between minor versions just as Ratatui’s does.

Example: a notes box with ratatui-textarea

The shelf TUI lets the reader attach a note to a book. ratatui-textarea provides the editor: the README’s minimal example creates a TextArea::default(), renders it by reference with render_widget(&textarea, area), passes key events to textarea.input(key) and reads the content back with textarea.lines(). Its default key bindings are Emacs-like (Ctrl+A and Ctrl+E for line start and end, Ctrl+K to delete to the end of the line, Ctrl+U and Ctrl+R for undo and redo, Ctrl+Y to paste), and input_without_shortcuts() turns them off so that the application can define its own. The crossterm backend is the default feature; termion and termwiz are selected by turning default features off.

// src/notes.rs
use ratatui::crossterm::event::{KeyEvent, KeyEventKind};
use ratatui::layout::Rect;
use ratatui::Frame;
use ratatui_textarea::TextArea;

pub struct NoteEditor {
    textarea: TextArea<'static>,
}

impl NoteEditor {
    pub fn new() -> Self {
        Self { textarea: TextArea::default() }
    }

    pub fn handle_key(&mut self, key: KeyEvent) {
        // Same guard as in the catalogue: Windows also reports releases.
        if key.kind == KeyEventKind::Press {
            self.textarea.input(key);
        }
    }

    pub fn render(&self, frame: &mut Frame, area: Rect) {
        frame.render_widget(&self.textarea, area);
    }

    pub fn text(&self) -> String {
        self.textarea.lines().join("\n")
    }
}

The TextArea<'static> lifetime parameter follows the tui-textarea design that the crate inherits, and the key type that input accepts depends on the enabled backend feature, so check the signature of your version on docs.rs before copying. The README does not state which Ratatui versions the crate supports in prose, only the dependency requirements listed above. The guard on KeyEventKind::Press is this page’s recommendation, taken from the Ratatui FAQ’s advice for Windows; the README does not discuss it.

Pitfalls of terminal-querying widgets

ratatui-image guesses the graphics protocol from environment variables and then asks the terminal with control sequences, and also queries the font size in pixels so image pixels map to character cells; if nothing works it falls back to half-block characters. Its documentation lists a Picker for detection and says its caveats include doing the query before Ratatui is initialised. A terminal that does not answer the query leaves a program waiting, so test on the terminals you support, and see Escape Sequences & Terminal Capabilities for how the queries work and why a multiplexer or an SSH hop can swallow them. The crate offers an image widget that is stateless and a stateful one whose resize and encoding block, and recommends running the latter on another thread or async task.

Beyond widgets: effects, components and frameworks

The Ratatui website’s ecosystem section lists larger building blocks. They are mentioned here as pointers; the website links their quickstarts and API documentation.

Project What the Ratatui documentation says about it

tachyonfx

An effects and animation library for Ratatui applications: simple effects composed and layered into transitions. Latest crates.io release 0.25.2.

ratcn

Themeable terminal UI components (buttons, lists, tabs, dialogs) inspired by shadcn/ui; an optional runtime routes focus, hover, keyboard and mouse events while the application keeps its own state and event loop.

tui-realm

A framework on top of Ratatui; see below.

Library or framework: Ratatui and tui-realm

The Ratatui FAQ answers the question directly. A library is something your code calls and control flow stays with the application; a framework inverts control and calls into code you plug in. Ratatui is the first kind: it does not dictate how the application is structured, it draws what you give it on each iteration, it does not read input (you use crossterm or another backend for that) and it leaves the event loop to you. tui-realm (4.1.0) is described as the second kind, with more guidelines about how the application should be structured and how data flows through it, in exchange for more features out of the box and, potentially, less application code.

Concern Ratatui alone tui-realm

Event loop

Written by the application (the architecture page shows polling, EventStream and Tokio versions)

Provided by the Application type, with an event listener and optional async ports

Structure of the code

Free: The Elm Architecture, components or Flux are all available

Components with props and state, messages and events, an Elm-inspired update routine, a view that handles mounting, unmounting and focus

Event routing

The application decides which widget gets a key

Components declare subscriptions that decide which events they receive

Backends

crossterm, termion, termwiz, and others

crossterm (default), termion and termwiz through features

Standard components

The widgets of ratatui-widgets

Provided by the separate tui-realm-stdlib crate

Dependency footprint

One crate family

Ratatui 0.30 plus its own layers; its major version is independent of Ratatui’s

Choose Ratatui alone for something the size of the shelf TUI: a list, a detail pane, a search box, a loans table. Consider a framework layer when many independent components each need focus and event routing, or when the team wants one prescribed structure instead of designing it. The cost is one more dependency whose upgrades must follow Ratatui’s, the same 0.x discipline described earlier.

The FAQ also compares Ratatui with Cursive, a library with a declarative UI and a built-in event loop that supports several backends; the contrast is the same: Ratatui leaves the loop, the state and the redrawing to the application.

Running Ratatui where there is no ordinary terminal

The crossterm backend writes to the process’s standard output, so a Ratatui program run inside an SSH session, a terminal multiplexer or a container works the way any full-screen program does: the remote side allocates a pseudo-terminal, sizes and key presses travel over the connection, and the program draws to it. What this involves (allocating a PTY with ssh -t, SIGWINCH forwarding, escape sequences passing through) is general terminal behaviour, covered on Terminals, TTYs & Pseudo-Terminals. What the Ratatui documentation does not provide is a guide to serving a Ratatui application to remote users from an SSH server written in Rust; the only SSH mention found in the Ratatui documentation is that the ratcn showcase can be tried with ssh ratcn.com. Anything built that way is outside what the official pages describe.

Ratatui’s rendering separates what is drawn from where it goes, which the ecosystem section uses.

Target Project What the Ratatui website says

Browser (WebAssembly)

Ratzilla 0.3.1

Builds terminal-themed web applications with Rust and WebAssembly; links a live demo, a quickstart, API documentation and examples. Needs Ratatui 0.30.1 or newer.

Embedded displays

mousefood 0.5.2

An embedded-graphics backend for Ratatui that supports no_std. Ratatui 0.30 added no_std support (disable default features and bring your own Backend), with ESP32, STM32H7, PSP and UEFI listed as tested.

UEFI (before the operating system starts)

ratatuefi

A uefi backend for Ratatui (no_std), used by an EFI boot application.

None of these is the right choice for the shelf tool on a laptop; they matter when the reason for choosing Ratatui is that the same widget code should also run in a browser demo or on a device. The cost is that every backend has its own input model, so the handle_key function above would be fed from a different event source.

Migrating

From tui-rs

Ratatui is a fork of tui-rs, created in February 2023 with the blessing of its original author to continue maintenance. The migration recipe offers three options: replace tui with ratatui everywhere (preferred, a find-and-replace of tui:: with ratatui:: plus a dependency edit), use ratatui as a drop-in replacement aliased to tui in Cargo.toml (a tui dependency that sets its package key to ratatui) and update the terminal crate, or support both through feature flags in a library. The recipe’s own Cargo.toml snippets use 0.28; today the first step after the rename is to move from there to the current version, one minor release at a time, through the table that follows. Code that targets the pre-fork tui-rs is several years of breaking changes behind current Ratatui.

Between Ratatui releases

Two official documents describe what changes between releases: the highlights pages on the website, one per minor release from v0.21.0 on, which present the new features with examples, and BREAKING-CHANGES.md, which lists every breaking change and how to migrate. The table samples the entries that most often touch an application like shelf.

Release Breaking changes recorded in BREAKING-CHANGES.md

0.30.0

Flex::SpaceAround now mirrors CSS flexbox; block::Title no longer exists; layout::Alignment is renamed layout::HorizontalAlignment; List::highlight_symbol accepts Into<Line>; Backend requires an associated Error type and a clear_region method; TestBackend uses Infallible as its error; disabling default features also disables the layout cache; Marker is non-exhaustive; MSRV 1.86.0.

0.30.1

Adding AsRef implementations for widgets can change type inference in rare cases; MSRV 1.88.0.

0.29.0

Sparkline::data takes an iterator of SparklineBar; Table::highlight_style is now Table::row_highlight_style; Tabs::select accepts Into<Option<usize>>; Color::from_hsl is behind the palette feature.

0.28.0

Frame::size is deprecated in favour of Frame::area; Backend::size returns Size; the Backend cursor methods are renamed; Ratatui requires crossterm 0.28.0.

0.27.0

List clamps the selected index; termion is updated to 4.0; changes to the prelude.

A reliable upgrade routine, using only Cargo and the tests described above:

  1. Read the highlights page and the BREAKING-CHANGES.md section for the target version first.

  2. Make sure the test suite passes before touching the version. Snapshot tests are the safety net: any unintended rendering change shows up as a snapshot diff.

  3. Change the version in Cargo.toml, run cargo update -p ratatui, and fix compile errors. Fix deprecation warnings too: the project removes deprecated items after at least two versions, so a warning left in place becomes an error one or two releases later.

  4. Run cargo tree -d to confirm there is one Ratatui and one crossterm in the dependency graph.

  5. Review the snapshot diffs with cargo insta review. Accept only the ones that are intended, for example a border or spacing change documented in the breaking-changes file.

  6. Upgrade third-party widget crates to releases that require the same Ratatui. If one has not been released for the new Ratatui, either wait, fork, or replace it.

  7. Repeat for each minor release instead of jumping several at once.

Pitfalls

  • Snapshot tests that depend on the clock, the working directory or an unsorted map fail intermittently; render from fixed data.

  • A snapshot records symbols only. A change that only alters colours or modifiers passes the snapshot test, so keep a Buffer or cell-level assertion for anything where style carries meaning (the selection highlight, error colours).

  • Widget and layout code that behaves differently depending on the terminal size needs a test at each size; one 80x24 snapshot does not cover a 40-column terminal.

  • Do not call ratatui::init or ratatui::run in tests: they put the real terminal in raw mode. Tests construct Terminal::new(TestBackend::new(...)) themselves, which is why the draw helper above takes the state and returns the terminal.

  • Unicode width: CJK characters and emoji occupy two cells, and a TestBackend snapshot shows the effect on the following cell. See the width section of Escape Sequences & Terminal Capabilities.

  • The Windows duplicate-key behaviour is invisible on macOS and Linux developer machines; keep the KeyEventKind::Press test in the suite so it is exercised on every platform.

  • Lock the minor version (a requirement of 0.30 on ratatui), commit Cargo.lock for applications, and upgrade deliberately.