Skip to content

Tab Widgets

Tabs

Horizontal tab bar with clickable tabs.

State-style setters use StyleSlot semantics: hover_style, focus_style, tab_hover_style, and active_style replace theme roles; use matching extend_*_style / inherit_*_style methods to extend or inherit scoped theme roles.

PropTypeDescription
tabsArc<[Tab]> / tabs_arcTab items (tabs(...) collects into Arc; prefer tabs_arc when sharing)
tab(Tab)methodAdd a single tab
activeusizeActive tab index
dividercharSeparator between tabs
capsOption<(char, char)>End-cap glyphs drawn around the active and hovered tabs, plus any tab opted in with Tab::capped(true). Each cap replaces one padding cell (the tab keeps its measured width), painted in the tab's own background over the strip background, so the tab reads as a rounded/pointed pill. None (default) keeps flat padding. A tab falls back to flat padding when it overflow-truncates, when its background matches the strip's, or when either cap is not single-width.
borderboolShow border
border_styleBorderStyleBorder style
paddingimpl Into<Padding>Padding
styleStyleBar idle style
focus_styleStyleBar focus style
extend_focus_style / inherit_focus_styleStyle / ()Extend or inherit the focus theme role instead of replacing it
hover_styleStyleBar hover style
extend_hover_style / inherit_hover_styleStyle / ()Extend or inherit the hover theme role instead of replacing it
tab_hover_styleStyleIndividual tab hover style
extend_tab_hover_style / inherit_tab_hover_styleStyle / ()Extend or inherit the tab hover theme role instead of replacing it
active_styleStyleActive tab style
extend_active_style / inherit_active_styleStyle / ()Extend or inherit the active-tab theme role instead of replacing it
disabledboolDisable all tabs
disabled_styleStyleStyle when disabled
focusableboolAccept focus
tab_stopboolInclude in sequential Tab traversal (default: true)
on_focus / on_blurCallback<()>Focus gained / lost
widthLengthWidth
heightLengthHeight
on_changeCallback<TabsEvent>Active tab changed
on_clickCallback<MouseEvent>Raw tab-bar click
on_keyKeyHandlerKey handler
rust
Tabs::new()
    .tabs(vec![
        Tab::new("Editor"),
        Tab::new("Terminal"),
        Tab::new("Log"),
    ])
    .active(self.active_tab)
    .border(true)
    .active_style(Style::new().fg(Color::Cyan).bold())
    .on_change(ctx.link().callback(|e: TabsEvent| Msg::TabChanged(e.index)))

Tab construction:

rust
Tab::new("Label")
    .style(Style::new().fg(Color::White))
    .disabled(false)

// A tab the widget cannot know is emphasized — an unsaved marker, an error state — carrying its own
// background. `capped(true)` shapes it like the active and hovered tabs instead of leaving it a flat
// colored block. The other cap conditions still apply: untruncated, a background distinct from the
// strip, and caps that fit the padding cells they replace.
Tab::new("Draft")
    .style(Style::new().fg(Color::Black).bg(Color::Yellow))
    .capped(true)

TabsEvent fields: index: usize


DraggableTabBar

Editor-style tab bar with drag reordering, per-tab close buttons, file icons, and cross-bar transfer.

When a close button is clicked with the mouse, the tab that moves into the closed tab's position temporarily inherits its width. This keeps the next close button under the pointer for rapid repeated closing. Natural or shrink-to-fit widths are restored as soon as the pointer leaves the tab bar. Action tabs such as a trailing + never inherit this lock.

PropTypeDescription
tabsVec<DraggableTab>Tab items
tab(DraggableTab)methodAdd a single tab
activeusizeActive tab index
on_actionCallback<DraggableTabActionEvent>Action tab clicked
variantDraggableTabBarVariantBordered or FrameLine
draggableboolEnable drag reordering
drag_previewboolFloating tab label near the pointer while dragging (default: true)
reorder_modeDragReorderModeLive or OnDrop
drag_thresholdu16Pixels before drag starts
show_close_buttonsboolShow close buttons
close_symbol&strClose button symbol
close_on_hover_onlyboolShow close only on hover
tab_max_widthOption<u16>Maximum tab label width in cells
overflowDraggableTabBarOverflowScroll or opt-in ShrinkThenScroll { min_tab_width }
scroll_wheelboolMouse wheel to scroll tabs
show_overflow_controlsboolShow < > overflow buttons
overflow_styleStyleOverflow button style
overflow_hover_styleStyleOverflow button hover style
overflow_left_labelFn(usize) -> Arc<str>Custom left overflow label from the hidden count
overflow_right_labelFn(usize) -> Arc<str>Custom right overflow label from the hidden count
empty_textimpl Into<Arc<str>>Placeholder when the bar has no tabs
empty_text_styleStyleEmpty-placeholder style
scroll_offsetusizeControlled scroll offset
show_file_iconsboolShow file type icons
file_icon_styleFileIconStyleIcon style (Nerd, NerdColored, Emoji)
file_icon_paletteFileIconPaletteCustom icon colors
file_icon_overrideHashMap<Arc<str>, FileIconOverride>Per-extension overrides
bar_id&strBar identifier for drag groups
drag_group&strGroup name for cross-bar transfer
styleStyleBar idle style
focus_styleStyleBar focus style
hover_styleStyleBar hover style
tab_hover_styleStyleHover style for inactive tabs (active tab keeps active_style)
active_styleStyleActive tab style (takes priority over hover)
close_styleStyleClose button style
close_hover_styleStyleClose button hover style
dividercharTab separator character
accent_symbolcharLeft/right accent character
active_accent_symbolcharActive tab accent
accent_styleStyleAccent style
active_accent_styleStyleActive tab accent style
borderboolShow border
border_styleBorderStyleBorder style
paddingimpl Into<Padding>Padding
disabledboolDisable all tabs
disabled_styleStyleStyle when disabled
focusableboolAccept focus
tab_stopboolInclude in sequential Tab traversal (default: true)
on_focus / on_blurCallback<()>Focus gained / lost
widthLengthWidth
heightLengthHeight
on_changeCallback<TabsEvent>Active tab changed
on_closeCallback<DraggableTabCloseEvent>Close button clicked
on_reorderCallback<DraggableTabReorderEvent>Tab dragged to new position
on_transferCallback<DraggableTabTransferEvent>Tab transferred to another bar
on_clickCallback<MouseEvent>Raw tab bar click
on_keyKeyHandlerKey handler

Tab Construction

rust
DraggableTab::new("main.rs")
    .closeable(true)
    .style(Style::new())
    .active_style(Style::new().fg(Color::Black).bg(Color::Cyan).bold())
    .hover_style(Style::new().bg(Color::indexed(238)))
    .accent_style(Style::new().fg(Color::indexed(244)))
    .active_accent_style(Style::new().fg(Color::Cyan).bold())
    .path("src/main.rs")         // For file icon auto-lookup
    .icon(Span::new("󰙱 ").fg(Color::Red))  // Manual icon override
    .right_badge(Span::new("M").fg(Color::Yellow))  // Git status marker

DraggableTab::active_style(...) patches over the bar's active_style for that tab, so global active defaults can still provide fields such as background while the tab overrides foreground or modifiers. hover_style(...) patches over DraggableTabBar::tab_hover_style(...) for inactive tabs. Active tabs keep active styling and do not receive hover styling. For the FrameLine variant, accent_style(...) and active_accent_style(...) patch over the bar accent styles for this tab's left marker.

Create pinned action tabs for tab-strip buttons such as +:

rust
DraggableTabBar::new()
    .tabs(files.iter().map(to_file_tab))
    .tab(DraggableTab::action("+").style(Style::new().fg(Color::LightGreen).bold()))
    .active(active_file_index)
    .on_change(ctx.link().callback(|e| Msg::SelectTab(e.index)))
    .on_action(ctx.link().callback(|_| Msg::CreateTab));

Action tabs render in the strip but do not emit on_change, become active, show close buttons, or participate as same-bar reorder targets. A trailing action tab stays pinned while normal tabs reorder before it.

To show a spinner in the icon position (replaces icon and file-icon when set):

rust
DraggableTab::new("Session 1")
    .leading(
        Spinner::new()
            .spinner_style(SpinnerStyle::Dots)
            .speed(SpinnerSpeed::Normal)
            .style(Style::new().fg(Color::Cyan))
            .into()
    )

Spinner label and layout properties are ignored in tabs because the tab owns its label and sizing.

The same slot also accepts Text elements for static fallbacks:

rust
DraggableTab::new("Session 1")
    .leading(Text::new("").style(Style::new().fg(Color::Cyan)).into())

The spinner glyph occupies the same slot as the icon - its width is determined by SpinnerStyle::width(). Tab spinners animate automatically with the app runtime. Spinner/text content takes priority over icon and file icons.

For attention-grabbing tabs, compose these same pieces on the tab item so the state travels with reorder and cross-bar transfer:

rust
DraggableTab::new("build")
    .leading(
        Spinner::new()
            .spinner_style(SpinnerStyle::Claude)
            .speed(SpinnerSpeed::Fast)
            .style(Style::new().fg(Color::Yellow).bold())
            .into()
    )
    .right_badge(Span::new("!").fg(Color::Yellow).bold())
    .active_style(Style::new().fg(Color::Black).bg(Color::Yellow).bold())
    .active_accent_style(Style::new().fg(Color::Yellow).bold())

If a tab needs custom blinking or pulsing, drive that from component state with a timer command and toggle the tab's style(...) or active_style(...) during view(). DraggableTabBar intentionally keeps the tab item as the owner of attention state rather than adding index-based alert styling.

Events

rust
// TabsEvent { index: usize }
// DraggableTabActionEvent { index: usize }
// DraggableTabCloseEvent { index: usize }
// DraggableTabReorderEvent { from: usize, to: usize }
// DraggableTabTransferEvent { from_bar: String, to_bar: String, from: usize, to: usize }

File Icons

rust
DraggableTabBar::new()
    .show_file_icons(true)
    .file_icon_style(FileIconStyle::NerdFontColored)
    .file_icon_palette(FileIconPalette { /* custom colors */ })

FileIconStyle variants: NerdFont, NerdFontColored, Emoji.

File icon is auto-resolved from DraggableTab::path(...). Use file_icon_override for per-extension customization.

Overflow

By default, DraggableTabBar keeps natural tab widths and scrolls horizontally when the tab strip overflows. To keep more tabs visible, opt into shrinking labels before scrolling:

rust
DraggableTabBar::new()
    .overflow(DraggableTabBarOverflow::ShrinkThenScroll { min_tab_width: 8 })

Tabs shrink only after their natural widths no longer fit the bar. If all tabs still cannot fit at min_tab_width, horizontal scrolling and overflow controls work as usual. Fixed tab affordances such as icons, badges, and close buttons are preserved even when they exceed the configured minimum.

The overflow controls render Nerd Font arrows with the hidden tab count. Apps that cannot rely on a Nerd Font (or that want a different shape) can replace either label:

rust
DraggableTabBar::new()
    .overflow_left_label(|hidden| Arc::from(format!(" <{hidden} ")))
    .overflow_right_label(|hidden| Arc::from(format!(" {hidden}> ")))

Each formatter receives the number of tabs hidden on that side. Padding is part of the returned string, and the control's width and hit target are measured from it, so a wider label shrinks the visible tab area by the same amount.

When the bar has no tabs, it stays blank by default (border/background still paint). Set a left-aligned placeholder with empty_text — useful for editor sidebars that temporarily have nothing open:

rust
DraggableTabBar::new()
    .empty_text("No open tabs")
    .empty_text_style(Style::default().dim())

The placeholder truncates with an ellipsis when wider than the bar and is not interactive. Prefer an action tab (for example +) when the empty state should accept clicks.

Cross-Bar Drag (Drag Groups)

rust
// Left editor bar
DraggableTabBar::new()
    .bar_id("left-editor")
    .drag_group("editors")
    .tabs(self.left_tabs.clone())
    .on_transfer(ctx.link().callback(Msg::TransferFromLeft))

// Right editor bar
DraggableTabBar::new()
    .bar_id("right-editor")
    .drag_group("editors")
    .tabs(self.right_tabs.clone())
    .on_transfer(ctx.link().callback(Msg::TransferFromRight))

Tabs can be dragged between bars that share the same drag_group. The on_transfer callback fires on the source bar and reports from_bar, to_bar, from index, and to index. After that callback queues the transfer, the destination bar's on_change callback fires with to, selecting the transferred tab through the same controlled-state path as a mouse click.

Full Example

rust
rsx! {
    DraggableTabBar {
        tabs: vec![
            DraggableTab::new("main.rs").closeable(true).path("src/main.rs"),
            DraggableTab::new("lib.rs").closeable(true).path("src/lib.rs"),
            DraggableTab::new("README.md").path("README.md"),
        ],
        bar_id: "editor",
        drag_group: "editors",
        active: self.active_tab,
        variant: DraggableTabBarVariant::FrameLine,
        tab_max_width: Some(20),
        overflow: DraggableTabBarOverflow::ShrinkThenScroll { min_tab_width: 8 },
        show_file_icons: true,
        file_icon_style: FileIconStyle::NerdFontColored,
        show_close_buttons: true,
        show_overflow_controls: true,
        active_style: Style::new().fg(Color::Cyan).bold(),
        on_change: ctx.link().callback(|e| Msg::SetActive(e.index)),
        on_close: ctx.link().callback(Msg::CloseTab),
        on_reorder: ctx.link().callback(Msg::ReorderTabs),
    }
}

Image Chip Bar Pattern

Use DraggableTabBar as an image attachment chip bar above TextArea:

rust
DraggableTabBar::new()
    .tabs(images.iter().enumerate().map(|(i, _)| {
        DraggableTab::new(format!("Image {}", i + 1))
            .closeable(true)
    }))
    .active(usize::MAX)       // No active tab (no highlight)
    .close_symbol("x")
    .draggable(false)
    .focusable(false)
    .on_close(ctx.link().callback(Msg::RemoveImage))
    .height(Length::Auto)

MPL-2.0