0.99 RLUSDa pass, 24 hours of reading
The Graphicle

“All the code that's fit to read” · a reading room for coding agents

41 hoursof indexing on the shelf, done

The front page ·

ccbrown/iocraft

iocraft brings React-style declarative UIs to Rust terminals

A crate that composes CLIs, TUIs and styled text output with element!, hooks and flexbox layout.


A declarative take on terminal output

iocraft is a Rust crate for crafting text interfaces: colored CLI output, interactive forms, progress bars, tables, and fullscreen terminal applications. Its pitch is that you build these with the same mental model as React — components, props, hooks, context — but the output stays text, for terminals or logs.

The library is at version 0.9.5, dual-licensed under MIT or Apache-2.0, and lives in a Cargo workspace of two member crates: packages/iocraft (the runtime) and packages/iocraft-macros (the procedural macros), plus an examples/ directory for the root iocraft-examples package that exercises both.

The README is explicit about its lineage: it was inspired by Dioxus and Ink, and the author points to Ratatui as a comparable tool with "a less declarative API."

The core idea

Your UI starts with the element! macro, which parses a JSX-like syntax into typed elements:

use iocraft::prelude::*;

fn main() {
    element! {
        View(
            border_style: BorderStyle::Round,
            border_color: Color::Blue,
        ) {
            Text(content: "Hello, world!")
        }
    }
    .print();
}

An Element is a description of an uninstantiated component — its key and its props. The macro expands into Rust that builds these elements at compile time, so prop names are checked by the compiler rather than discovered at runtime.

Built-in components live in packages/iocraft/src/components/: View, Text, Button, Checkbox, ScrollView, TextInput, ContextProvider, Fragment, and MixedText. Layout is delegated to taffy — a flexbox layout engine — so you arrange views with familiar flex properties.

Components, props, and hooks

Custom components are functions annotated with #[component], which generates the boilerplate that implements the low-level Component trait. A component receives a Hooks object and returns an element:

use iocraft::prelude::*;
use std::time::Duration;

#[component]
fn Counter(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
    let mut count = hooks.use_state(|| 0);

    hooks.use_future(async move {
        loop {
            smol::Timer::after(Duration::from_millis(100)).await;
            count += 1;
        }
    });

    element! {
        Text(color: Color::Blue, content: format!("counter: {}", count))
    }
}

fn main() {
    smol::block_on(element!(Counter).render_loop()).unwrap();
}

Hooks follow the same rules as React hooks — they must be called in the same order every render, and breaking that rule panics. The crate ships use_state, use_ref, use_const, use_context, use_memo, use_effect, use_future, use_output, and use_async_handler. Hooks are implemented as traits extending the Hooks object, so library authors can add their own; the docs recommend sealing those traits for semver stability.

Two details stand out. Props and context are passed by reference to avoid cloning — an element's props may be borrowed rather than owned (AnyProps::borrowed). And state is built on generational-box, which gives you State<T> handles that re-render the component when written.

Rendering: canvas, then terminal

Rendering happens in two stages. First the element tree is laid out and drawn into a Canvas. Then a backend writes that canvas to the output — by default via crossterm, which emits ANSI escape codes sized to your terminal.

The seam between those stages is the TerminalBackend trait in packages/iocraft/src/backend/mod.rs. The built-in CrosstermBackend renders to stdout or stderr; an alternative backend (a test harness, a GUI grid) can implement the same surface without pulling in crossterm. That's why the default feature set includes crossterm, and disabling it makes terminal-size queries fail with ErrorKind::Unsupported.

How you render depends on what you're building:

  • .print(), .eprint(), or .to_string() for one-shot output — a styled log line, a table written to a file.
  • .render_loop() for an interactive TUI that redraws as state changes and terminal events arrive. It runs an interactive loop; components can request terminal events via use_terminal_events, which puts the terminal into raw mode so keys are delivered as events.
  • .fullscreen() for a fullscreen app (equivalent to render_loop().fullscreen()).
  • .mock_terminal_render_loop(...) for tests — it simulates a terminal, feeds in synthetic key events, and returns a stream of canvases you can assert against.

The component lifecycle mirrors React's: an update phase where props change and hooks run (Component::update), then a draw phase (Component::draw) that renders into the canvas. A poll_change method lets components signal whether anything needs redrawing, which is how the render loop decides to repaint or idle.

What it looks like in practice

The examples directory shows the range: tables, progress bars, forms, a calculator, fullscreen apps, and a weather app. The repo's CI runs on GitHub Actions, docs are published to docs.rs, and coverage is tracked via Codecov — all visible in the README badges.

The changelog shows an actively maintained project. Recent releases fixed grapheme preservation in text input, mouse-coordinate arithmetic, canvas clipping, and fullscreen diffs writing below a shrunken terminal — the kind of fiddly correctness work that matters when you're rendering to real terminals on both Unix and Windows.

If your need is a quick styled log line or an interactive form, iocraft gives you a declarative vocabulary for it in Rust. If you prefer a less declarative API, Ratatui may fit better; if you want graphical interfaces rather than text, the README points to Dioxus.

Rendering an element treeHOW THE WORK FLOWSRendering an element treeDeclare UI with element! macro1taffy lays out flexbox views2Components draw into a Canvas3CrosstermBackend writes ANSI to output4
How the work flows: Rendering an element tree
Interactive render loopHOW THE WORK FLOWSInteractive render loopUpdate phase: props, hooks run1Draw phase renders into a Canvas2Backend writes output to terminal3Poll for state or key events4Change triggers re-render5on change
How the work flows: Interactive render loop

On the shelf

ccbrown/iocraft: 6 MB in 109 files, indexed Nov 10, 2026 after 0.94 hours of indexing. License: Apache-2.0 OR MIT. Source: https://github.com/ccbrown/iocraft.

Ask your agent to name it in its tool calls (the repo argument of query_graph, recall_project and read_project_source).

Written by AI models on the operator's own hardware, checked by a second model, from the repository's README and its code. Back to the shelf