Components

DhTabs

Underline-indicator tab strip for switching between sibling views. Variant (primary/secondary/tertiary/error) × alignment (start/center/end/stretch) × scrollable. Renders only the strip — caller owns selectedIndex and the content below.

Basic — primary variant, stretch alignment

The defaults: variant=Primary draws the active label and 3.dp pill indicator in --color-primary; alignment=Stretch gives each tab equal width.

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. Paparazzi can't resolve Compose Multiplatform `Res.drawable.*` headlessly, so the snapshot uses Material `ImageVector` icons; in your code use the Davidhorn icon set.

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 = { Icon(painterResource(Res.drawable.ic_camera), null) }, contentDescription = "Video"),
        DhTabs.Item(icon = { Icon(painterResource(Res.drawable.ic_image), null) }, contentDescription = "Photos"),
        DhTabs.Item(icon = { Icon(painterResource(Res.drawable.ic_microphone_on), null) }, 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 = { Icon(painterResource(Res.drawable.ic_camera), null) }),
        DhTabs.Item("Photos", icon = { Icon(painterResource(Res.drawable.ic_image), null) }),
        DhTabs.Item("Audio", icon = { Icon(painterResource(Res.drawable.ic_microphone_on), null) })
    )
)

Alignment — start

Tabs use their intrinsic width and the strip hugs the leading edge of the container.

Light dh-tabs tabs, primary, align, start (light theme)
Dark dh-tabs tabs, primary, align, start (dark theme)
DhTabs(
    selectedIndex = current,
    onTabSelected = { current = it },
    tabs = items,
    alignment = DhTabs.Alignment.Start
)

Alignment — center

Tabs use their intrinsic width and the strip centres within the container.

Light dh-tabs tabs, primary, align, center (light theme)
Dark dh-tabs tabs, primary, align, center (dark theme)
DhTabs(
    selectedIndex = current,
    onTabSelected = { current = it },
    tabs = items,
    alignment = DhTabs.Alignment.Center
)

Alignment — end

Tabs use their intrinsic width and the strip hugs the trailing edge.

Light dh-tabs tabs, primary, align, end (light theme)
Dark dh-tabs tabs, primary, align, end (dark theme)
DhTabs(
    selectedIndex = current,
    onTabSelected = { current = it },
    tabs = items,
    alignment = DhTabs.Alignment.End
)

Scrollable overflow

When the strip is wider than its container, alignment ≠ Stretch + scrollable=true gives a 90.dp-min horizontal scroller.

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,
    alignment = DhTabs.Alignment.Start,
    scrollable = true
)

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)
    )
)