Components

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.

Light dh-tabs tabs, primary, label, stretch (light theme)
Dark dh-tabs tabs, primary, label, stretch (dark theme)
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.

Light dh-tabs tabs, secondary, label, stretch (light theme)
Dark dh-tabs tabs, secondary, label, stretch (dark theme)
DhTabs(
    selectedIndex = current,
    onTabSelected = { current = it },
    tabs = items,
    variant = DhTabs.Variant.Secondary
)

Variant — tertiary

Selected text and indicator pick up --color-tertiary.

Light dh-tabs tabs, tertiary, label, stretch (light theme)
Dark dh-tabs tabs, tertiary, label, stretch (dark theme)
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).

Light dh-tabs tabs, error, label, stretch (light theme)
Dark dh-tabs tabs, error, label, stretch (dark theme)
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.

Light dh-tabs tabs, primary, icon, stretch (light theme)
Dark dh-tabs tabs, primary, icon, stretch (dark theme)
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).

Light dh-tabs tabs, primary, label, icon, stretch (light theme)
Dark dh-tabs tabs, primary, label, icon, stretch (dark theme)
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).

Light dh-tabs tabs, primary, scrollable, overflow (light theme)
Dark dh-tabs tabs, primary, scrollable, overflow (dark theme)
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.

Light dh-tabs tabs, primary, disabled (light theme)
Dark dh-tabs tabs, primary, disabled (dark theme)
DhTabs(
    selectedIndex = current,
    onTabSelected = { current = it },
    tabs = listOf(
        DhTabs.Item("Active"),
        DhTabs.Item("Archive"),
        DhTabs.Item("Premium", enabled = false)
    )
)