Skip to content

Tabs

A row of tabs, one selected at a time. Left and Right move between them; Enter, Space, or a click switches.

rust
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(...).

rust
use ratcn::TabsActivation;

Tabs::new(tabs)
    .selection(|s: &AppState| s.selected, Msg::ScreenSelected)
    .activation(TabsActivation::Automatic)

Large

TabsSize::Large gives a taller tab shape.

rust
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.

rust
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(...):

rust
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:

rust
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.