Skip to content

Dev tab bar

In review
Live · web

Editor tabs: dirty dots yield to the close X on hover · Delete closes the focused tab · overflow fades

export function NockerlChatInput() {
  // the floating pill
}

Closed 0 tabs · 6 open. The X is pointer-only; keyboard users press Delete on a focused tab (the ARIA deletable-tabs pattern). The island is live.

The web console’s editor tab strip, deliberately not a new component: it is the shipped Tabs composite carrying two dev-tab extensions (the extend-never-fork rule):

  • onClose makes every tab closable: a quiet X for pointer users (out of the tab order) and Delete / Backspace on the focused tab for keyboard users, the ARIA deletable-tabs pattern. The list stays controlled: the callback only reports the value; the host removes the tab and moves selection.
  • dirty (per tab) renders the editor’s unsaved dot in the trailing slot; with a closable tab it yields to the X on hover/focus (an opacity cross-fade, frozen under prefers-reduced-motion).
  • Overflow needs nothing new: Tabs’ own scrollable tablist + edge fades take over when the strip outgrows its container.
import { NockerlTabs } from '@dizyx/nockerl-react';
<NockerlTabs
label="Open files"
size="sm"
tabs={files.map((f) => ({ value: f.path, label: f.name, dirty: f.unsaved }))}
value={activePath}
onChange={setActivePath}
onClose={(path) => closeFile(path)} // host removes the tab + moves selection
/>

The full surface lives on the Tabs page; the dev-tab additions:

PropTypeDefaultDescription
tabs *TabItemDef[]The tabs, left-to-right. Each owns a panel rendered via renderPanel or its own panel.
value *stringThe active tab value (controlled). Exactly one tab is selected.
onChange(next: string) => voidFired with the next value. Ignored for a disabled tab.
label *stringAccessible name for the whole tablist (wired via aria-label).
variantNockerlTabsVariant'underline'Underline indicator (default) vs. 'enclosed': the same cyan underline seated inside a recessed well.
sizeNockerlTabsSize'md'Tab height + padding + type role.
renderPanel(value: string) => ReactNodeRenders the body for the active tab. Falls back to the tab's own panel.
onClose((value: string) => void) | undefinedCLOSABLE tabs (task 2656 dev-tab-bar): when set, every tab grows a trailing close affordance (pointer) and DELETE / BACKSPACE closes the focused tab (the keyboard half of the ARIA deletable-tabs pattern; the X itself stays out of the tab order). The list stays CONTROLLED: this only reports the value; the host removes the tab + moves selection.

Also accepts the native <div> attributes (e.g. onClick, disabled, id, aria-*, data-*), forwarded straight through, plus a forwarded ref.