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.
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
});