Dockable
Guides

Tab overflow

When a strip has more tabs than fit - the tabs that do not fit go to your menu, the selected one stays; or wrap and scroll.

When a strip has more tabs than it can show, Dockable hides the ones that do not fit, one by one as the strip narrows, and always keeps the selected tab in view (the VS Code behaviour). You list the hidden tabs in a menu of your own, opened by Dockable.TabOverflowTrigger.

Intermediate exampleOverflow to a selectWhen the tabs no longer fit, only the ones that do not fit leave the strip, one by one as it narrows, into a Fragiola Select; the selected tab always stays. Picking a tab from the select brings it into the strip. The engine measures; the example renders the select.Open the live example

1. Let the strip overflow

Nothing to switch on: Dockable.TabList measures itself and its tabs. Give it a size the tabs can overflow. In a header row, let it shrink:

<div className="flex">
    <Dockable.TabList className="flex min-w-0 flex-1 overflow-hidden gap-1 ps-1">
        {(tab) => <Dockable.Tab node={tab}>{tab.getName()}</Dockable.Tab>}
    </Dockable.TabList>
    {/* the trigger, step 2 */}
</div>

The engine compares the tabs' sizes (and the list's gap) with the list's inner size (its size minus padding and border). The tabs fit from the start of the strip; when the selected tab is past the end, tabs are taken off the end until it fits, and it shows in their place. Down to a single visible tab: the selected one.

A tab that does not fit gets display: none and data-overflow-hidden, and the tab list gets data-overflowing. The arrow keys still reach a hidden tab: moving onto it selects it, which brings it into the strip.

2. Add the trigger and your menu

Dockable.TabOverflowTrigger renders only while tabs are hidden, next to the tab list. The engine reserves the space it takes, so tabs and trigger never overlap. It is a button with no text and no menu: make it the trigger of yours, and list the hidden tabs from useTabOverflow:

import { Actions, type TabSetNode } from "@fragiola/dockable";
import { Dockable, useDockable, useTabOverflow } from "@fragiola/dockable-react";

function OverflowMenu({ tabset }: { tabset: TabSetNode }) {
    const { engine } = useDockable();
    const { hidden } = useTabOverflow(tabset);
    return (
        <Menu.Root>
            <Dockable.TabOverflowTrigger aria-label="More tabs" render={<Menu.Trigger />}>
                {`+${hidden.length}`}
            </Dockable.TabOverflowTrigger>
            <Menu.Content>
                {hidden.map((tab) => (
                    <Menu.Item key={tab.getId()} onClick={() => engine.doAction(Actions.selectTab(tab.getId()))}>
                        {tab.getName() || tab.getAltName()}
                    </Menu.Item>
                ))}
            </Menu.Content>
        </Menu.Root>
    );
}

Selecting a hidden tab (Actions.selectTab, through onAction) brings it into the strip; another tab goes to the menu in its place. altName is "the name used in the overflow menu when the tab has no name (e.g. an icon-only tab)". The label key DockableLabel.Overflow_Menu_Tooltip names the trigger when you pass getLabel to the root.

The overflow-select example uses Fragiola UI's Select, with Dockable.TabOverflowTrigger rendered as its trigger.

It works everywhere tabs do

  • Borders: a side border's vertical tab list hides along its height.
  • Popout windows: each window measures its own strips.
  • Drag and drop: a drop into a strip lands between the visible tabs; hidden tabs take no drop position.

Wrap or scroll instead

A strip that wraps (flex-wrap: wrap, several rows of tabs; the tabset's enableTabWrap attribute is a natural switch) or scrolls (overflow-x: auto) keeps every tab: turn tab overflow off with overflow={false} on the tab list. For a scrolling strip, scroll the selected tab into view when the selection changes:

function ScrollingTab({ tab }: { tab: TabNode }) {
    const ref = useRef<HTMLElement>(null);
    const selected = tab.isSelected();
    useEffect(() => {
        if (selected) ref.current?.scrollIntoView({ block: "nearest", inline: "nearest" });
    }, [selected]);
    return (
        <Dockable.Tab node={tab} ref={ref}>
            {tab.getName()}
        </Dockable.Tab>
    );
}

Tabs that shrink (flex-shrink: 1 with a minimum width, a truncated label) combine with either: they shrink first, and overflow once they reach their minimum.

Keep the strip's start padding in every case: a drop before the first tab needs it (gap 10).

Your own measuring

The decision itself is a pure function of the core, for a custom strip:

import { computeTabOverflow } from "@fragiola/dockable";

const { visible, hidden } = computeTabOverflow({
    available: 420, // the strip's inner size
    sizes: [96, 80, 120, 88], // each tab's natural size, in model order
    gap: 4,
    selectedIndex: 3,
    reserve: 32, // the trigger's space, used only when something is hidden
});

On this page