DhTabs
Underline-indicator tab strip for switching between sibling views, built on Material3 TabRow — the indicator slides and resizes between tabs as selection changes. Variant (primary/secondary/tertiary/error) × scrollable. Renders only the strip (caller owns selectedIndex and the content), or use the pageContent overload to bundle a swipeable HorizontalPager whose drag the indicator follows.
Basic — primary variant
The defaults: variant=Primary draws the active label and 3.dp pill indicator in --color-primary, and the tabs share the width equally. The indicator slides between tabs as selection changes.


var current by remember { mutableStateOf(0) }
DhTabs(
selectedIndex = current,
onTabSelected = { current = it },
tabs = listOf(
DhTabs.Item("Templates"),
DhTabs.Item("Incomplete"),
DhTabs.Item("Completed")
)
)Variant — secondary
Selected text and indicator pick up --color-secondary.


DhTabs(
selectedIndex = current,
onTabSelected = { current = it },
tabs = items,
variant = DhTabs.Variant.Secondary
)Variant — tertiary
Selected text and indicator pick up --color-tertiary.


DhTabs(
selectedIndex = current,
onTabSelected = { current = it },
tabs = items,
variant = DhTabs.Variant.Tertiary
)Variant — error
Selected text and indicator pick up --color-error. Use for destructive-flow tabs (e.g. failed-items panes).


DhTabs(
selectedIndex = current,
onTabSelected = { current = it },
tabs = items,
variant = DhTabs.Variant.Error
)Icon-only configuration
Pass only the icon slot on each Item. Provide contentDescription for screen readers.


DhTabs(
selectedIndex = current,
onTabSelected = { current = it },
tabs = listOf(
DhTabs.Item(icon = { DhIcon(DhIcons.Camera, null, decorative = true) }, contentDescription = "Video"),
DhTabs.Item(icon = { DhIcon(DhIcons.Image, null, decorative = true) }, contentDescription = "Photos"),
DhTabs.Item(icon = { DhIcon(DhIcons.Mic, null, decorative = true) }, contentDescription = "Audio")
)
)Label + icon configuration
Pass both label and icon on each Item — the tab stacks them vertically at a 64.dp min-height (Figma label-icon row spec).


DhTabs(
selectedIndex = current,
onTabSelected = { current = it },
tabs = listOf(
DhTabs.Item("Video", icon = { DhIcon(DhIcons.Camera, null, decorative = true) }),
DhTabs.Item("Photos", icon = { DhIcon(DhIcons.Image, null, decorative = true) }),
DhTabs.Item("Audio", icon = { DhIcon(DhIcons.Mic, null, decorative = true) })
)
)Scrollable overflow
When the strip is wider than its container, scrollable=true lays tabs out at their intrinsic width in a 90.dp-min horizontal scroller (ScrollableTabRow).


DhTabs(
selectedIndex = current,
onTabSelected = { current = it },
tabs = manyTabs,
scrollable = true
)Swipeable (HorizontalPager)
The content-hosting overload drops selectedIndex / onTabSelected and takes a pageContent lambda. It bundles a HorizontalPager: tapping a tab animates to that page, swiping moves the selection, and the underline follows the swipe gesture frame by frame. (No preview — the swipe animation and the hosted page content can't be shown in a static snapshot; run the demo to see it.)
// Content-hosting overload: no selectedIndex / onTabSelected —
// the bundled HorizontalPager owns the selection, and a swipe
// moves both the page and the indicator.
DhTabs(
tabs = listOf(
DhTabs.Item("Overview"),
DhTabs.Item("Activity"),
DhTabs.Item("Settings")
)
) { page ->
when (page) {
0 -> OverviewContent()
1 -> ActivityContent()
else -> SettingsContent()
}
}Disabled tab
Tabs gated behind a permission or feature flag.


DhTabs(
selectedIndex = current,
onTabSelected = { current = it },
tabs = listOf(
DhTabs.Item("Active"),
DhTabs.Item("Archive"),
DhTabs.Item("Premium", enabled = false)
)
)