Tabs
A row of tabs, one selected at a time. Left and Right move between them; Enter, Space, or a click switches.
use ratcn::{Tab, Tabs};
let tabs = Tabs::new([
Tab::new(Screen::Overview, "Overview"),
Tab::new(Screen::Analytics, "Analytics"),
Tab::new(Screen::Reports, "Reports"),
])
.tab_focus(|s: &AppState| s.focused, Msg::ScreenFocusChanged)
.selection(|s: &AppState| s.selected, Msg::ScreenSelected);
ctx.render_component("tabs", tabs, tabs_area);
match state.selected {
Screen::Overview => render_overview(ctx, content_area),
Screen::Analytics => render_analytics(ctx, content_area),
Screen::Reports => render_reports(ctx, content_area),
}The row draws only the tabs — what appears below is yours, matched on the selected value in the same frame. Tabs are identified by your own values rather than by position, so filtering or reordering the row keeps the same tab selected.
Activation
Whether arrow keys switch tabs or only move a cursor. It matters when switching is expensive or destructive: manual lets the user look before committing.
Manual is the default. TabsActivation::Automatic drops the separate cursor and selects immediately, so it needs only .selection(...).
use ratcn::TabsActivation;
Tabs::new(tabs)
.selection(|s: &AppState| s.selected, Msg::ScreenSelected)
.activation(TabsActivation::Automatic)Large
TabsSize::Large gives a taller tab shape.
use ratcn::TabsSize;
Tabs::new(tabs).size(TabsSize::Large)Use .height() and .width() for layout constraints rather than hard-coding numbers. A large row given fewer than three rows paints nothing and is excluded from keyboard and pointer interaction. If the supplied area is taller than the tabs, only the first one or three rows participate; blank excess rows are not focus or click targets.
Disabled
Disabled tabs render dimmed, ignore clicks, and are skipped by arrow keys. A selected tab that becomes disabled stays visibly selected, so the panel on screen always has an identifiable tab.
Tab::new(Screen::Reports, "Reports").disabled(!state.reports_enabled)Current state is in scope while declaring, so pass the flag directly.
Overflow
When the row is narrower than its tabs, the widget keeps the selected tab visible and marks the hidden sides with ‹ and ›. Even a one-cell-wide row remains interactive; the selected or focused tab clips to the available width. Nothing to configure.
Styling
The selected tab uses the theme's default-button colors and the rest use secondary-button colors, so a tab row stays consistent with the buttons around it. Override one row with .style(...):
use ratcn::TabsStyle;
Tabs::new(tabs).style(|theme| {
let mut style = TabsStyle::from_theme(theme);
style.selected_background = theme.accent;
style
})TabsStyle::fallback() is the no-theme starting point: plain ANSI colors that render on any terminal.
Paint-Only Widget
TabsWidget draws a row without focus or events. It is an ordinary Ratatui widget, so it works in a plain Ratatui app with no Ratcn runtime. Everything is addressed by index rather than by value — which tab is selected, which the cursor is on (.focused_tab(...)), which are disabled, and whether the row itself has focus:
use ratcn::TabsWidget;
frame.render_widget(
TabsWidget::new(&["Overview", "Analytics", "Reports"])
.selected(Some(selected_index))
.focused_tab(Some(cursor_index))
.disabled(&[false, true, false])
.focused(row_has_focus)
.themed(&theme),
area,
);Full API
Every method, with binding requirements and edge-case detail: Tabs, Tab, TabsWidget, TabsActivation, TabsSize, TabsStyle.
See Also
For tabs coordinating several screens, each with its own state and messages, see Structuring a larger app.