Tabs

Organize multiple screens into a tabbed interface with keyboard navigation.

let active = state!(cx, || 0);

View::tabs()
    .tab("Overview", overview_content)
    .tab("Settings", settings_content)
    .tab("About", about_content)
    .active(active.get())
    .on_change(with!(active => move |idx| active.set(idx)))
    .build()

Run with: cargo run -p telex-tui --example 14_tabs

Basic tabs

Tabs display one of several screens based on which tab is active:

View::tabs()
    .tab("Home", View::text("Welcome home!"))
    .tab("Profile", View::text("Your profile"))
    .tab("Settings", View::text("App settings"))
    .build()

The first tab ("Home") is active by default.

Controlling active tab

Track which tab is visible with state:

let active_tab = state!(cx, || 0);  // 0 = first tab

View::tabs()
    .tab("Tab 1", content1)
    .tab("Tab 2", content2)
    .tab("Tab 3", content3)
    .active(active_tab.get())
    .on_change(with!(active_tab => move |idx: usize| {
        active_tab.set(idx);
    }))
    .build()

When the user switches tabs, on_change fires with the new index.

Tab content

Each tab's content is a full View. You can put anything inside:

View::tabs()
    .tab("List", View::list().items(items).build())
    .tab("Table", View::table().rows(rows).build())
    .tab("Form",
        View::vstack()
            .child(View::text_input()...)
            .child(View::button()...)
            .build()
    )
    .active(active.get())
    .on_change(on_change)
    .build()

Keyboard navigation

Tabs support multiple navigation methods:

  • ←/→ - Previous/next tab
  • [ and ] - Previous/next tab (alternative)
  • 1, 2, 3, ... - Jump to specific tab by number

All methods wrap around (last → first, first → last).

Per-tab state

State persists even when tabs aren't visible:

let active_tab = state!(cx, || 0);

// These states persist across tab switches
let settings_notify = state!(cx, || true);
let settings_theme = state!(cx, || "dark".to_string());
let profile_name = state!(cx, || String::new());

View::tabs()
    .tab("Profile",
        View::text_input()
            .value(profile_name.get())
            .on_change(with!(profile_name => move |s| profile_name.set(s)))
            .build()
    )
    .tab("Settings",
        View::vstack()
            .child(View::checkbox()
                .label("Notifications")
                .checked(settings_notify.get())
                .on_toggle(with!(settings_notify => move |c| settings_notify.set(c)))
                .build())
            .build()
    )
    .active(active_tab.get())
    .on_change(with!(active_tab => move |idx| active_tab.set(idx)))
    .build()

When you switch to Settings and back to Profile, the input value is still there.

Dynamic tab content

Rebuild tab content based on state:

let data = state!(cx, Vec::new);

View::tabs()
    .tab("Data", View::list().items(data.get()).build())
    .tab("Add",
        View::button()
            .label("Add Item")
            .on_press(with!(data => move || {
                data.update(|v| v.push("New".into()));
            }))
            .build()
    )
    .active(active.get())
    .on_change(on_change)
    .build()

Adding an item in the "Add" tab updates the list shown in the "Data" tab.

Programmatic tab switching

Switch tabs from code:

// Switch to second tab
active_tab.set(1);

// Button that switches tabs
View::button()
    .label("Go to Settings")
    .on_press(with!(active_tab => move || active_tab.set(2)))
    .build()

Tab bar styling

Tab labels appear in a row at the top. The active tab is highlighted.

The widget handles all styling automatically - you just provide labels.

Number of tabs

You can have as many tabs as you want, but consider usability:

  • 2-5 tabs - Ideal, easy to navigate
  • 6-8 tabs - Acceptable, fits most terminals
  • 9+ tabs - Gets crowded, consider nested tabs or a different UI

Empty tabs

You can have an empty tab:

.tab("Coming Soon", View::styled_text("This feature is under development").dim().build())

Real-world example

Settings screen with multiple categories:

let active_tab = state!(cx, || 0);
let notifications = state!(cx, || true);
let dark_mode = state!(cx, || true);
let auto_save = state!(cx, || true);

View::tabs()
    .tab("Overview",
        View::vstack()
            .child(View::styled_text("Welcome!").bold().build())
            .child(View::text("Configure your app using the tabs above"))
            .build()
    )
    .tab("Settings",
        View::vstack()
            .child(View::checkbox()
                .label("Enable notifications")
                .checked(notifications.get())
                .on_toggle(with!(notifications => move |c| notifications.set(c)))
                .build())
            .child(View::checkbox()
                .label("Dark mode")
                .checked(dark_mode.get())
                .on_toggle(with!(dark_mode => move |c| dark_mode.set(c)))
                .build())
            .child(View::checkbox()
                .label("Auto-save")
                .checked(auto_save.get())
                .on_toggle(with!(auto_save => move |c| auto_save.set(c)))
                .build())
            .build()
    )
    .tab("About",
        View::vstack()
            .child(View::text("My App v1.0"))
            .child(View::text("Built with Telex"))
            .build()
    )
    .active(active_tab.get())
    .on_change(with!(active_tab => move |idx| active_tab.set(idx)))
    .build()

Tabs in modals

Tabs work inside modals:

View::modal()
    .visible(show_settings.get())
    .title("Settings")
    .child(
        View::tabs()
            .tab("General", general_settings)
            .tab("Advanced", advanced_settings)
            .active(settings_tab.get())
            .on_change(on_tab_change)
            .build()
    )
    .build()

Conditional tabs

Show different tabs based on state:

let is_admin = state!(cx, || false);

let mut tabs_view = View::tabs()
    .tab("Home", home_content)
    .tab("Profile", profile_content);

if is_admin.get() {
    tabs_view = tabs_view.tab("Admin", admin_content);
}

tabs_view
    .active(active.get())
    .on_change(on_change)
    .build()

Note: Be careful with conditional tabs and active index - ensure the index stays valid.

Tips

Active is zero-indexed - First tab is 0, second is 1, etc.

State persists - All tabs render simultaneously (only one is visible). State in inactive tabs is preserved.

Labels should be short - Keep tab labels to 10-15 characters. Long labels crowd the tab bar.

Don't nest tabs - Tabs within tabs is confusing. Use a different navigation pattern.

Consider order - Put the most important/frequently used tab first.

Number shortcuts - Users can press 1, 2, 3 to jump to tabs. Design with this in mind.

Wrap around - Left arrow on first tab goes to last tab. Right arrow on last tab goes to first.

Tab bar always visible - The tab labels stay at the top while the content scrolls.

Next: Forms