limn
    ↑↓ ChooseEnter OpenEsc Close
    GitHub

    Components/Navigation

    Tabs

    A row of tabs for switching between views, with a selection that slides to the active one. TabStrip is the other kind: folder tabs for open resources, such as files, that scroll sideways and open onto the body beneath them.

    Native componentGuide source

    Preview

    CodeSource
    fn view(state: &State, palette: ComponentPalette) -> Element {
        let tabs = vec![
            Tab::new("view=files", "Files", 120.0).icon(Icon::Folder).active(state.view == "files"),
            Tab::new("view=changes", "Changes", 120.0).icon(Icon::GitBranch).active(state.view == "changes"),
        ];
        Tabs::new(tabs, palette).id("views").render()
    }
    
    fn update(state: &mut State, action: &UiAction) {
        if let UiAction::Clicked { id } = action {
            if let Some(view) = id.strip_prefix("view=") {
                state.view = view.to_owned();
            }
        }
    }

    Examples

    Each one is the library drawing it. Press, type and hover.

    Tabs2 / 2
    CodeSource
    row(vec![Tabs::new(tabs("nav-fill"), palette)
        .id("nav-fill")
        .fill_width(cx.width.min(520.0))
        .render()])
    Underlined tabs1 / 2
    CodeSource
    row(vec![Tabs::new(tabs("nav-under"), palette)
        .variant(TabsVariant::Underlined)
        .accent(state.theme().accent_text())
        .gap(16.0)
        .id("nav-under")
        .render()])
    Underlined tabs2 / 2
    CodeSource
    Tabs::new(
        VIEWS
            .iter()
            .map(|(id, label, _)| {
                let chosen = state.choice("nav-under-text", VIEWS[0].0);
                Tab::new(format!("nav-under-text={id}"), *label, 72.0)
                    .active(*id == chosen)
                    .enabled(*id != "history")
            })
            .collect(),
        palette,
    )
    .variant(TabsVariant::Underlined)
    .gap(24.0)
    .id("nav-under-text")
    .render()
    Resource tabs1 / 6
    CodeSource
    file_tabs(cx, "nav-files-1", 1)
    Resource tabs2 / 6
    CodeSource
    file_tabs(cx, "nav-files-3", 3)
    Resource tabs3 / 6
    CodeSource
    file_tabs(cx, "nav-files-12", 12)
    Resource tabs4 / 6
    CodeSource
    section_tabs(cx)
    Resource tabs5 / 6
    CodeSource
    single_tab(cx)
    Resource tabs6 / 6
    CodeSource
    status_tabs(cx)

    Tabs

    A row of tabs for switching between views, with a selection that slides to the active one. TabStrip is the other kind: folder tabs for open resources, such as files, that scroll sideways and open onto the body beneath them.

    Import

    use limn::{Component, Icon, Tab, Tabs, TabsVariant};

    For folder tabs:

    use limn::{file_icon, Component, TabItem, TabStrip};

    Usage

    Tabs are controlled: mark the active tab each frame and change it when a tab reports a click. A Tab takes an action ID, a label and a width, which is never less than 40.

    fn view(state: &State, palette: ComponentPalette) -> Element {
        let tabs = vec![
            Tab::new("view=files", "Files", 120.0).icon(Icon::Folder).active(state.view == "files"),
            Tab::new("view=changes", "Changes", 120.0).icon(Icon::GitBranch).active(state.view == "changes"),
        ];
        Tabs::new(tabs, palette).id("views").render()
    }
    
    fn update(state: &mut State, action: &UiAction) {
        if let UiAction::Clicked { id } = action {
            if let Some(view) = id.strip_prefix("view=") {
                state.view = view.to_owned();
            }
        }
    }

    id names the strip for its selection animation. Set it when tabs are inserted, removed or reordered; without it the strip is named after its first tab.

    Underlined

    Tabs::new(tabs, palette)
        .variant(TabsVariant::Underlined)
        .accent(theme.accent_text())
        .gap(16.0)
        .render()

    variant chooses how the active tab is marked. TabsVariant::Segmented, the default, fills it on a rounded track. TabsVariant::Underlined draws text tabs with no track: a 2 px underline under the active tab, over a 1 px hairline in the palette’s border. The underline slides as the selection does. Both variants have the same size, so one can replace the other without moving anything around them. In a column the hairline runs the full width it is given; in a row it is as wide as the tabs.

    accent colors the active tab’s label and the underline. Without it they take the palette’s text. Pass a color that reads as text, such as a theme’s accent_text().

    gap sets the space between two tabs, 2 by default, never below 0. The underline spans its tab’s whole width, so underlined tabs usually want a wider gap and a width close to the label’s. Set it before fill_width, which reads it.

    Fill a width

    Tabs::new(tabs, palette).fill_width(520.0).render()

    fill_width(total) gives every tab the same width so the track is total wide, whatever width each tab was built with.

    Height

    size takes a ControlSize and height a number, never less than 24. The default is the height of ControlSize::Dense.

    Disabled tabs

    Tab::new("view=history", "History", 120.0).disabled()

    A disabled tab is drawn muted and cannot be chosen. Marked active, it takes no selection fill and no underline.

    Folder tabs

    let items = vec![
        TabItem::new("file=0", "file-extent-0", file_icon("main.rs"), "main.rs", 112.0)
            .tooltip("crates/limn/src/main.rs")
            .active(state.file == 0)
            .close(close_button),
        TabItem::new("file=1", "file-extent-1", file_icon("lib.rs"), "lib.rs", 112.0)
            .active(state.file == 1),
    ];
    let strip = TabStrip::new("files", items, palette).render();
    TabStrip::header(strip, tools, width, palette).render()

    A TabItem takes its ID, an extent ID, an icon, a title and a width. The host gives every tab its width; the title fades when it does not fit. The extent ID names the whole tab, close slot included, which is what the strip scrolls into view. TabStrip::new takes a scroll ID that keeps the scroll position between frames. The strip scrolls sideways when its tabs do not fit and keeps the active tab in view. A click on a tab reports UiAction::Clicked with the tab’s ID.

    TabStrip::header(leading, trailing, width, palette) is the 32 px bar that holds the strip and, in trailing, the tools that stay outside the scrolling part. side_inset sets the space at each edge of the bar, 6 by default, and gap the space between the tabs and the tools, 8 by default.

    The palette’s surface paints the active tab, and must match the body under the strip; raised paints the bar and hover an inactive tab under the pointer.

    Close slot and title ink

    close puts an element of the host’s in the tab’s trailing slot, 24 px square. It should be 20 px square so its hover stays inside the tab’s rounded corner. The slot takes any element, so it can also hold a mark of unsaved changes. Without one the slot stays empty.

    TabItem::new("file=0", "file-extent-0", icon, "lib.rs", 112.0).title_ink(warning)

    title_ink colors the title alone, active or not, for a state the host marks by color, such as a file’s status in version control. tooltip sets the hint’s text, which is the title by default, and tooltip_placement where it opens, above the tab by default.

    A menu on each tab

    TabStrip::new("files", items, palette).each_tab(|index, tab| {
        ContextMenu::new(format!("file-menu-{index}"), tab, entries(index), palette).render()
    })

    each_tab wraps each rendered tab, by position, in something the host owns. The wrapper must keep the tab’s size.

    Label-only and single tabs

    let tab = TabStrip::surface("panel=output", label, active, 96.0, palette);
    let tabs = TabStrip::align(Element::row(vec![tab]).size(Some(96.0), Some(TabStrip::TAB_HEIGHT)));

    TabStrip::surface is the clickable surface of one tab around content of your own, 24 px tall, for tabs that are only a label or an icon. TabStrip::align sets a row of tabs on the bottom of the bar. For a group that shows a single resource, TabItem::render(palette) gives the tab alone.

    Disabled folder tabs

    TabItem::new("file=2", "file-extent-2", icon, "build.log", 112.0).enabled(false)

    A disabled folder tab keeps its place, its tooltip and its close slot, with icon and title in muted ink, and takes no pointer or focus. The close control is the host’s to leave out.

    Keyboard

    With the focus on a tab, in either kind; a disabled tab is stepped over:

    KeyEffect
    Left, RightChooses the tab before or after, wrapping at the ends
    Home, EndChooses the first or the last tab

    Motion

    The selection of Tabs, the fill or the underline, slides to the active tab over the move_ms motion token. animation_ms takes another duration.

    Actions

    ActionWhen
    UiAction::Clicked { id }A tab is chosen by pointer or keyboard. The ID is the tab’s.

    Accessibility

    A TabItem of a TabStrip is a tab named by its title, not by its tooltip nor its close control, and says whether it is the selected one, the active tab. A disabled tab is there and cannot be used. A tab built with TabStrip::surface is named by the text in its content.

    A Tab of Tabs is a tab named by its label, and says whether it is the active one. A disabled tab is there and cannot be used.

    Limits

    • The underline is 2 px and the hairline 1 px. They are not tokens.
    • The underline spans the tab’s width, which the host gives; tabs are not sized to their labels.

    API reference

    The guide covers composition, options and actions. Generate the complete Rust type and builder reference from the checkout:

    cargo doc -p limn --open

    Component API overview · State and actions

    The example is drawn by crates/limn-gallery/src/pages/navigation.rs.