Widgets overview
ftui-widgets contains roughly 80 widget implementations, but it is not a
miscellaneous grab-bag the way many widget crates accrete over time. Every
widget either consumes a Rect and fills it (a pure Widget) or consumes a
Rect and mutably updates a piece of state (a StatefulWidget). That is the
whole API surface. The remaining 60,000+ lines of code are concrete
implementations on top of two traits.
This page orients you to what is in the box and how it fits into the broader frame pipeline. If you want the exact trait signatures with file:line citations, jump to the traits page. If you want the full A-Z inventory, jump to the catalog.
Why a curated library, not a framework
Most TUI widget crates end up at one of two extremes:
- A grab-bag. A dumping ground of weakly-related helpers with no shared vocabulary. Widgets can’t compose because each one invents its own state-management story.
- A framework. An all-or-nothing abstraction that forces you into its render loop, its event model, and its layout engine.
FrankenTUI takes a third path: a curated, stateful-first library sitting on
top of a tiny trait surface. Widgets don’t drive the frame — the
runtime does. Widgets don’t own their own I/O — the
presenter does. Widgets don’t diff themselves — the
buffer diff does. Their sole job is to turn (model, area)
into buffer writes.
That minimalism is what lets 80 widgets coexist without stepping on each
other: a Block can wrap a Table which contains an Input which is
anchored to a Popover, and the composition is just nested render(area, frame) calls.
The stateless / stateful distinction
Every widget picks one of two traits. The distinction is load-bearing:
| Trait | Purpose | Example |
|---|---|---|
Widget | Pure render. Given (area, frame), draw cells. No persistent state. | Paragraph, Block, Rule, Badge |
StatefulWidget | Render with a mutable State. During render, the widget is allowed to clamp, adjust scroll offset, or cache layout. | List, Table, Tree, VirtualizedList, Tabs, Scrollbar |
The default choice is Widget. You only reach for StatefulWidget when the
render pass itself must mutate state — typically to keep a selected row
visible as the viewport resizes, or to memoise an expensive layout decision
across frames.
The state in StatefulWidget is held by you (the caller), not the widget.
Widgets are cheap to construct every frame — only the state is long-lived.
This is the opposite of most object-oriented widget kits.
See the traits page for the exact signatures and an
explanation of what each parameter (area, frame, state) gives you.
Where widgets live in the render pipeline
A widget’s render() call is one small step inside a much larger pipeline:
The widget layer sees only steps B and C. It receives a Rect
(already allocated by layout), writes into Frame.buffer, and returns. It
never touches ANSI, never calls write!() on stdout, never computes a diff.
That separation is what makes widgets trivially testable: given a known area
and a synthetic Frame, the widget’s output is a deterministic function. You
can unit-test a Table by constructing a Frame, calling render, and
asserting on the resulting Buffer. No terminal required.
Widgets are re-created every frame — they are meant to be cheap value
objects. If you find yourself caching a Block across frames, you are
probably holding it wrong. Cache the data that goes into the block, not
the block itself.
Degradation and is_essential
Widgets declare whether they are essential. The Widget trait provides a
default is_essential(&self) -> bool { false }; text inputs and primary
content areas override it to return true.
When the frame runs over budget (see frame budget)
the degradation level rises: Full → SimpleBorders → NoStyling → EssentialOnly → Skeleton. At EssentialOnly, the frame only renders
widgets that declare is_essential() == true. Decorative widgets
(gradients, sparklines, ornamental borders) are silently skipped.
This means your Input for a search box keeps working even on a 3-FPS
virtual terminal on a 16-year-old server, while the candy around it quietly
fades.
Quick shape of a widget
A minimal stateless widget:
use ftui_widgets::Widget;
use ftui_render::{Rect, frame::Frame};
struct Tag<'a> { label: &'a str }
impl Widget for Tag<'_> {
fn render(&self, area: Rect, frame: &mut Frame) {
let text = format!("[{}]", self.label);
frame.buffer.draw_text(area.x, area.y, &text, Default::default());
}
fn is_essential(&self) -> bool { false }
}A minimal stateful widget:
use ftui_widgets::StatefulWidget;
struct Counter;
#[derive(Default)]
struct CounterState { pub n: u64 }
impl StatefulWidget for Counter {
type State = CounterState;
fn render(&self, area: Rect, frame: &mut Frame, state: &mut CounterState) {
state.n = state.n.wrapping_add(1); // allowed: render may mutate
let s = state.n.to_string();
frame.buffer.draw_text(area.x, area.y, &s, Default::default());
}
}Both snippets are end-to-end correct: no traits to derive, no macros, no registration.
What makes this library distinctive
A handful of widgets do things that are genuinely hard to find elsewhere:
A Bayesian evidence ledger ranks matches by posterior probability. The scoring model is documented and auditable.
Command paletteFenwick-tree prefix sums plus a Bayesian height predictor render 100K rows at 60 FPS.
Virtualized listA real stack with focus traps, input capture, and entrance/exit animations.
Modal stackA focus graph with Tab ordering, spatial neighbours, and history for undo/redo of focus changes.
Focus managerPitfalls
- Rendering outside
view(). Widgets can only register hit regions, cursor position, and link IDs during the view phase. CallingWidget::renderfromupdate()silently corrupts frame state. - Holding
StatefulWidgetstate in the widget. The trait deliberately separates them. If you embed state in the widget, you have re-invented React’s single-store footgun. - Ignoring
is_essential(). AtEssentialOnlydegradation, a widget that returnsfalsesimply won’t render. Inputs and primary outputs must override the default.