Tabs
Underline tabs: icons + a count badge; the underline slides, the panel swaps (Tab in, Arrow keys)
Enclosed tabs: the sliding cyan underline, seated inside a recessed well (not a filled pill)
A disabled tab: skipped by arrow keys, still legible
Scrollable / overflow: many tabs scroll horizontally with edge fades
Small size: same indicator, tighter rhythm
Active section: overview · enclosed chat · workspace overview. Each tab swaps its panel below. The island is live.
// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform;// this is the canonical API the published @dizyx/nockerl-react package exposes.)import { NockerlTabs } from '@dizyx/nockerl-react';
export function SessionPanel() { const [tab, setTab] = useState('overview'); return ( <NockerlTabs label="Session sections" value={tab} onChange={setTab} tabs={[ { value: 'overview', label: 'Overview', icon: 'overview' }, { value: 'activity', label: 'Activity', icon: 'activity' }, { value: 'files', label: 'Files', icon: 'files', count: 12 }, { value: 'settings', label: 'Settings', icon: 'settings', disabled: true }, ]} renderPanel={(v) => <SectionBody section={v} />} /> );}
// Enclosed variant (the same cyan underline, inside a recessed well). Pass variant="enclosed".<NockerlTabs label="Panel sections" variant="enclosed" value={tab} onChange={setTab} tabs={tabs} renderPanel={renderPanel} />// Android: Jetpack Compose (canonical). Material 3 SecondaryTabRow: a MUTED// underline tab row (NOT the all-cyan PrimaryTabRow, which Nockerl retired;// inactive tabs stay muted, the active tab gets the cyan label + the cyan// indicator). Pair with a HorizontalPager so each tab owns its panel.import androidx.compose.material3.SecondaryTabRowimport androidx.compose.material3.Tabimport androidx.compose.material3.TabRowDefaultsimport androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
val tabs = listOf("Overview", "Activity", "Files", "Settings")val pagerState = rememberPagerState { tabs.size }val scope = rememberCoroutineScope()val colors = LocalNockerlColors.current
SecondaryTabRow( selectedTabIndex = pagerState.currentPage, containerColor = colors.cardAlt, indicator = { TabRowDefaults.SecondaryIndicator( Modifier.tabIndicatorOffset(pagerState.currentPage), color = colors.accentPrimary, // the cyan signature line ) },) { tabs.forEachIndexed { i, title -> Tab( selected = pagerState.currentPage == i, onClick = { scope.launch { pagerState.animateScrollToPage(i) } }, text = { Text(title) }, selectedContentColor = colors.accentPrimary, unselectedContentColor = colors.onCardAltMuted, ) }}HorizontalPager(state = pagerState) { page -> SectionBody(tabs[page]) }
// Many tabs → ScrollableTabRow for a horizontally-scrolling, edge-fading row.// macOS: SwiftUI (canonical). The bespoke first-class NockerlTabs (v1.15.0): a controlled// tablist + a CONTAINED content panel, the container the native TabView lacked (B12 amended// 2026-07-17). The panel is panelStyle-driven (v1.16.0): .well (default) is a recessed// NockerlWell(.container) GROUND that cards LIFT OFF (matches StylesView); .card is a lifted// surface for plain content. a11y-first: roving Arrow/Home/End, typeahead, Delete-to-close,// VoiceOver selected state.NockerlTabs( tabs: [ NockerlTabItem(value: "overview", label: "Overview", icon: AnyView(Image(systemName: "square.grid.2x2"))), NockerlTabItem(value: "activity", label: "Activity", icon: AnyView(Image(systemName: "waveform.path.ecg")), statusDot: .success), // optional per-tab marker (active-engine), != selection NockerlTabItem(value: "files", label: "Files", icon: AnyView(Image(systemName: "folder")), count: 12), ], selection: $section, // controlled, exactly one selected label: "Project sections") { value in panel(for: value) // the active tab's body, in the content panel (a recessed well by default)}// variant: .underline (default) | .enclosed · size: .sm | .md// panelStyle: .well (default, recessed ground cards lift off) | .card (lifted surface for plain content)// onClose: for closable tabsParameters
Section titled “Parameters”| Prop | Type | Default | Description |
|---|---|---|---|
tabs * | TabItemDef[] | The tabs, left-to-right. Each owns a panel rendered via renderPanel or its own panel. | |
value * | string | The active tab value (controlled). Exactly one tab is selected. | |
onChange | (next: string) => void | Fired with the next value. Ignored for a disabled tab. | |
label * | string | Accessible name for the whole tablist (wired via aria-label). | |
variant | NockerlTabsVariant | 'underline' | Underline indicator (default) vs. 'enclosed': the same cyan underline seated inside a recessed well. |
size | NockerlTabsSize | 'md' | Tab height + padding + type role. |
renderPanel | (value: string) => ReactNode | Renders the body for the active tab. Falls back to the tab's own panel. | |
onClose | ((value: string) => void) | undefined | CLOSABLE 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.
| Parameter | Type | Default | Description |
|---|---|---|---|
selectedTabIndex * | Int | Index of the active tab. Drives the indicator position (usually pagerState.currentPage). | |
indicator | @Composable -> Unit | SecondaryIndicator | The moving indicator. Use TabRowDefaults.SecondaryIndicator tinted accentPrimary with tabIndicatorOffset(...), the cyan signature line. |
containerColor | Color | surface | Tab-row background. Bind to a Nockerl surface (e.g. cardAlt), never the all-cyan default. |
selected * | Boolean | On Tab: whether this tab is active. Drives the selectedContentColor (cyan) vs unselectedContentColor (muted). | |
onClick * | () -> Unit | On Tab: invoked when tapped, typically pagerState.animateScrollToPage(i). | |
enabled | Boolean | true | On Tab: when false, the tab is dimmed and non-interactive. |
HorizontalPager | Composable | The swipeable panel host. Each page is one tab’s tabpanel; pagerState keeps the row + pager in sync. |
The bespoke first-class NockerlTabs (v1.15.0): a CONTROLLED tablist + a contained
content panel, a11y-first, ported 1:1 from the web/Compose NockerlTabs. (The system
TabView was retired for Nockerl surfaces; see B12-amend 2026-07-17.)
| Parameter | Type | Default | Description |
|---|---|---|---|
tabs * | [NockerlTabItem] | The tabs left-to-right. Each item carries value / label / optional icon (any view) / count / disabled / dirty / statusDot. | |
selection * | Binding<String> | The active tab value: controlled, exactly one selected. | |
label * | String | Accessible name for the whole tablist. | |
variant | NockerlTabsVariant | .underline | .underline (a sliding cyan line on a baseline hairline) or .enclosed (the same underline seated in a recessed well). |
size | NockerlTabsSize | .md | Tab height + padding + type role: .sm or .md. |
panelStyle | NockerlTabsPanelStyle | .well | How the content panel reads (v1.16.0): .well is a recessed FLAT ground (NockerlWell(.container)) that liftable cards sit ON. This is the default, so a panel holding cards reads with depth, not a card-on-card blend. .card is a lifted surface for plain content. |
onClose | ((String) -> Void)? | nil | When set, tabs grow a close affordance and Delete closes the selected tab (reports the value; the host removes it + moves selection). |
panel * | (String) -> View | The body for a tab value. The active tab body renders in the content panel. |