CLI tool
ccbrown/iocraft avatar
ccbrown/iocraft

iocraft is React for terminals, and Ratatui is the same problem solved without the abstraction

A Rust crate for beautiful, artisanally crafted CLIs, TUIs, and text-based IO.

1,567 stars67 forksRustApache-2.0

At a glance

What is it?
iocraft is a Rust crate that renders declarative component trees to a terminal or to plain text, using flexbox layout from taffy and React style hooks. The README itself points at Ratatui as the same job with a less declarative API.
Who is it for?
Reach for iocraft when you are writing an interactive terminal application in Rust and want component composition, hooks and layout resolution rather than manual cursor arithmetic, particularly if you already think in React terms. Do not reach for it to decorate a log line, because the same library has a lighter mode for that and a full render loop is the wrong cost for structured logging.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The React model, applied to a character grid

iocraft's premise is stated plainly in the README: if you are familiar with React you will feel at home, because it uses the same concepts but is text focused and made for Rust. The unit of construction is the element! macro, which declares UI in a React or SwiftUI-like syntax, and a component is a function.

The first program is short enough to read in full. A View with a rounded border in blue wraps a Text, and the whole expression is printed:

rust
use iocraft::prelude::*;

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

What that renders is a bordered box in the terminal, and the same tree can be printed as ASCII anywhere else. That duality is the design goal in the opening line: crafting text output and interfaces for the terminal or logs. The crate is not only for full screen programs, and the feature list says as much: colored and styled output to the terminal or ASCII output anywhere else.

Built-in components include View, Text and TextInput, and you define your own with the #[component] macro. The list of examples in the repository is a better catalogue of the library's range than the feature list is: tables, progress bars, fullscreen apps, forms, overlapping elements, a calculator, scrolling views, and a weather example, alongside context.rs, use_input.rs and use_output.rs for the hook surface.

Layout comes from taffy, which changes what you can express

The feature list credits flexbox layout powered by the taffy crate, and that is the single most consequential design choice in the library. It means iocraft is not asking you to place things by cursor coordinates. You describe containers and the constraints between their children, and a layout engine resolves them into rows, columns, sizes and positions on the character grid.

That is a real difference in what you can build. Coordinates give you exact placement and no reflow. Flexbox gives you a view that survives a terminal resize, a longer string, or a different panel size, at the cost of not being able to say exactly where a character goes.

The trade matters most for the kinds of programs the examples show. A table, a form and a progress bar all want to respond to a changing width. A fixed dashboard or a status line that assumes 80 columns does not, and for that a cursor-addressing approach with no layout engine is less machinery.

The other feature worth naming is passing props and context by reference to avoid unnecessary cloning. In a terminal renderer, the element tree is rebuilt and re-rendered repeatedly, so ownership of a value that never changes matters more than it would in a GUI toolkit where the tree is built once.

print for output, render_loop for a program

The two entry points in the examples are not interchangeable, and the difference is the single most useful thing to understand before writing anything.

Calling .print() on an element, as the hello world does, renders the tree and writes it out. No event loop, no input handling, no redraw. That is the mode for a command that composes a block of styled output once: a report, a formatted result, a log block, output piped to a file or another program. The same tree can be emitted as ASCII text, which is what makes it usable where a terminal is not present at all.

Calling render_loop instead runs the application. The counter example ends with smol::block_on(element!(Counter).render_loop()).unwrap(), which keeps rendering and processing events until the program ends, and what you see is a Text element whose content updates twice a second. That is the mode for anything that reacts to keys.

The build step is not a separate step either. The feature list calls out building fullscreen terminal applications with ease, and fullscreen.rs sits in the examples directory next to the rest, so the full screen mode is a property of the same rendering path rather than a different library.

The upstream project is on crates.io as iocraft with documentation on docs.rs, so adding it to a project is a dependency declaration like any other crate. The README does not include a cargo add line, so the crates.io page is the documented source for the exact version to pin.

Hooks: state, futures and the shape of a component

Custom components take a Hooks value and return an element, which is the mechanism that makes state survive re-renders. The counter example is compact enough to show the whole pattern:

rust
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))
    }
}

Three things to notice. The initializer for use_state is a closure, so the initial value is computed once rather than on every render. use_future takes an async block and runs it, here an infinite loop that awaits a 100 millisecond timer from the smol crate and then mutates the state. And the closure captures the state handle by move, which is why the component signature takes mut hooks.

The async story is worth noticing for a terminal program. The timer comes from smol, and the whole program is driven by smol::block_on. Input and output are hooks too, which is what use_input.rs and use_output.rs in the examples demonstrate.

If you are coming from React, the mental translation is direct. If you are not, the parts to internalise are that state lives in a hook, effects are futures, and rendering repeats.

Two crates, two version lines

The repository root is a Cargo workspace with two members, packages/iocraft and packages/iocraft-macros, and the root package is iocraft-examples at version 0.1.0 with edition 2021. The examples are a package with their own dev-dependencies rather than being a member of the workspace, which is a common arrangement that keeps the published crates free of example dependencies.

Those dev-dependencies also tell you what the examples need: futures, smol, chrono, unicode-width, anyhow, surf with the h1-client feature, serde, serde_json and mexprp.

The versioning detail that will bite someone is that the main crate and the macro crate version independently. Recent releases are iocraft-v0.9.3 on 2026-10-14, iocraft-v0.9.2 on 2026-10-11 and iocraft-macros-v0.2.5 on 2026-10-11, so the macros crate is on its own 0.2 line while the library sits at 0.9. The element! and #[component] macros come from the separate crate, which means a dependency bump can be needed for the macros even when the library version is unchanged.

Build tooling uses Makefile.toml rather than Makefile, and CI is wired through GitHub Actions with coverage tracked on Codecov.

Ratatui is the same job with a different API shape

The README names its own alternatives, which is more useful than an outside comparison would be. It credits Dioxus and Ink as inspirations, the latter for people building graphical interfaces or working in JavaScript and TypeScript. It then points at Ratatui as serving a similar purpose with a less declarative API.

That sentence is the whole comparison. Both libraries render structured terminal interfaces in Rust.

For a Rust developer arriving from React, the learning curve is part of the decision, and iocraft's README is candid that the model is borrowed. For a Rust developer who has already written a terminal renderer, the abstraction is something to weigh against rather than a default. The README also states broad support for both Unix and Windows terminals, which is a compatibility claim worth testing against your own target terminals rather than taking on trust.

Licence, cadence and what the project does not claim

iocraft is licensed under either of the Apache License, Version 2.0 or the MIT license, at your option, with LICENSE-APACHE and LICENSE-MIT in the repository root. The contribution terms state that unless you say otherwise, contributions are dual licensed on the same terms with no additional conditions. No legal advice is offered, but the practical read is that there is no licence negotiation to do, and the choice is yours at the dependency line.

Cadence is fast and visible. The last push was on 2026-10-14, the same day as release iocraft-v0.9.3, and v0.9.2 landed three days earlier. The repository is not archived. A 0.9 line on a library with a macro crate is pre-1.0, which conventionally signals that the API may still change between minor versions, and the README does not describe a compatibility policy or a deprecation window.

The gaps in the documentation are worth listing so an evaluator does not discover them late. The README does not document a minimum supported Rust version, does not explain what the h1-client feature on surf is for, does not describe how terminal detection and colour fall back when the output is not a terminal, and does not cover accessibility or screen reader behaviour, which for a TUI library is a question the documentation leaves open entirely.

Editorial conclusion

Reach for iocraft when you are writing an interactive terminal application in Rust and want component composition, hooks and layout resolution rather than manual cursor arithmetic, particularly if you already think in React terms. Do not reach for it to decorate a log line, because the same library has a lighter mode for that and a full render loop is the wrong cost for structured logging. Verify first that the crate name and the macro crate line up, since the workspace publishes iocraft and iocraft-macros separately with independent version numbers, and note that the dual MIT or Apache-2.0 grant means you choose.

Frequently asked questions

How do I add iocraft to a Rust project?

The crate is published on crates.io as iocraft, with API documentation on docs.rs. The README does not include a cargo add command, so the crates.io page is where you find the version to depend on.

What is the difference between .print() and render_loop in iocraft?

Calling .print() on an element renders it once and writes it out, which is the path for styled output and logs. Calling render_loop runs the application with event handling and redraws, as the counter example does with smol::block_on(element!(Counter).render_loop()).

How does iocraft decide where things go on screen?

Layout uses flexbox powered by the taffy crate rather than explicit cursor coordinates, so views respond to the terminal size. The examples directory includes table.rs, form.rs, progress_bar.rs, fullscreen.rs and scrolling.rs to show the range of layouts this produces.

What licence is iocraft released under?

Dual licensed, at your option, under Apache License Version 2.0 or the MIT license, with both license files in the repository root. Contributions are dual licensed the same way unless a contributor states otherwise.

Official sources

  1. ccbrown/iocraft on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ccbrown-iocraft.svg)](https://hysenlabs.com/projects/ccbrown-iocraft)