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.
| Prop | Type | Description |
|---|---|---|
tabs | Arc<[Tab]> / tabs_arc | Tab items (tabs(...) collects into Arc; prefer tabs_arc when sharing) |
tab(Tab) | method | Add a single tab |
active | usize | Active tab index |
divider | char | Separator between tabs |
caps | Option<(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. |
border | bool | Show border |
border_style | BorderStyle | Border style |
padding | impl Into<Padding> | Padding |
style | Style | Bar idle style |
focus_style | Style | Bar focus style |
extend_focus_style / inherit_focus_style | Style / () | Extend or inherit the focus theme role instead of replacing it |
hover_style | Style | Bar hover style |
extend_hover_style / inherit_hover_style | Style / () | Extend or inherit the hover theme role instead of replacing it |
tab_hover_style | Style | Individual tab hover style |
extend_tab_hover_style / inherit_tab_hover_style | Style / () | Extend or inherit the tab hover theme role instead of replacing it |
active_style | Style | Active tab style |
extend_active_style / inherit_active_style | Style / () | Extend or inherit the active-tab theme role instead of replacing it |
disabled | bool | Disable all tabs |
disabled_style | Style | Style when disabled |
focusable | bool | Accept focus |
tab_stop | bool | Include in sequential Tab traversal (default: true) |
on_focus / on_blur | Callback<()> | Focus gained / lost |
width | Length | Width |
height | Length | Height |
on_change | Callback<TabsEvent> | Active tab changed |
on_click | Callback<MouseEvent> | Raw tab-bar click |
on_key | KeyHandler | Key handler |
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:
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.
| Prop | Type | Description |
|---|---|---|
tabs | Vec<DraggableTab> | Tab items |
tab(DraggableTab) | method | Add a single tab |
active | usize | Active tab index |
on_action | Callback<DraggableTabActionEvent> | Action tab clicked |
variant | DraggableTabBarVariant | Bordered or FrameLine |
draggable | bool | Enable drag reordering |
drag_preview | bool | Floating tab label near the pointer while dragging (default: true) |
reorder_mode | DragReorderMode | Live or OnDrop |
drag_threshold | u16 | Pixels before drag starts |
show_close_buttons | bool | Show close buttons |
close_symbol | &str | Close button symbol |
close_on_hover_only | bool | Show close only on hover |
tab_max_width | Option<u16> | Maximum tab label width in cells |
overflow | DraggableTabBarOverflow | Scroll or opt-in ShrinkThenScroll { min_tab_width } |
scroll_wheel | bool | Mouse wheel to scroll tabs |
show_overflow_controls | bool | Show < > overflow buttons |
overflow_style | Style | Overflow button style |
overflow_hover_style | Style | Overflow button hover style |
overflow_left_label | Fn(usize) -> Arc<str> | Custom left overflow label from the hidden count |
overflow_right_label | Fn(usize) -> Arc<str> | Custom right overflow label from the hidden count |
empty_text | impl Into<Arc<str>> | Placeholder when the bar has no tabs |
empty_text_style | Style | Empty-placeholder style |
scroll_offset | usize | Controlled scroll offset |
show_file_icons | bool | Show file type icons |
file_icon_style | FileIconStyle | Icon style (Nerd, NerdColored, Emoji) |
file_icon_palette | FileIconPalette | Custom icon colors |
file_icon_override | HashMap<Arc<str>, FileIconOverride> | Per-extension overrides |
bar_id | &str | Bar identifier for drag groups |
drag_group | &str | Group name for cross-bar transfer |
style | Style | Bar idle style |
focus_style | Style | Bar focus style |
hover_style | Style | Bar hover style |
tab_hover_style | Style | Hover style for inactive tabs (active tab keeps active_style) |
active_style | Style | Active tab style (takes priority over hover) |
close_style | Style | Close button style |
close_hover_style | Style | Close button hover style |
divider | char | Tab separator character |
accent_symbol | char | Left/right accent character |
active_accent_symbol | char | Active tab accent |
accent_style | Style | Accent style |
active_accent_style | Style | Active tab accent style |
border | bool | Show border |
border_style | BorderStyle | Border style |
padding | impl Into<Padding> | Padding |
disabled | bool | Disable all tabs |
disabled_style | Style | Style when disabled |
focusable | bool | Accept focus |
tab_stop | bool | Include in sequential Tab traversal (default: true) |
on_focus / on_blur | Callback<()> | Focus gained / lost |
width | Length | Width |
height | Length | Height |
on_change | Callback<TabsEvent> | Active tab changed |
on_close | Callback<DraggableTabCloseEvent> | Close button clicked |
on_reorder | Callback<DraggableTabReorderEvent> | Tab dragged to new position |
on_transfer | Callback<DraggableTabTransferEvent> | Tab transferred to another bar |
on_click | Callback<MouseEvent> | Raw tab bar click |
on_key | KeyHandler | Key handler |
Tab Construction
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 markerDraggableTab::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 +:
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):
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:
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:
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
// 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
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:
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:
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:
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)
// 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
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:
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)