
iOS-style segmented control offering platform-appropriate visuals with three built-in styles, animated selection indicator, per-segment disabling, equal/content widths, keyboard navigation, RTL and extensive customization.
An iOS-style segmented control for Compose Multiplatform — platform-appropriate visuals with three styles built in, an animated selection indicator, and full keyboard & RTL support.
Material 3's SingleChoiceSegmentedButtonRow always looks like Material — which feels out of place
on iOS, where users expect the familiar UISegmentedControl. SegmentedControl gives you a
platform-appropriate look from a single Compose codebase: ship the iOS style on Apple platforms,
Material3 on Android, or Pill everywhere — without forking your UI code.
| Platform | Supported | Tested |
|---|---|---|
| Android | ✅ | ✅ (unit + UI) |
| iOS | ✅ | ✅ (UI, Skiko) |
| Desktop | ✅ | ✅ (unit + UI) |
| Web | ✅ | ✅ (compile + logic) |
gradle/libs.versions.toml:
[libraries]
segmented-control = { module = "io.github.nadeemiqbal:segmented-control", version = "0.1.0" }commonMain dependencies:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.segmented.control)
}
}
}var selected by remember { mutableStateOf(0) }
SegmentedControl(
items = listOf("Day", "Week", "Month"),
selectedIndex = selected,
onSelectionChange = { selected = it },
modifier = Modifier.fillMaxWidth(),
)Icon + text segments
SegmentedControl(
items = listOf(
SegmentItem("List", Icons.Default.List),
SegmentItem("Grid", Icons.Default.GridView),
),
selectedIndex = selected,
onSelectionChange = { selected = it },
)Pick a style
SegmentedControl(
items = items,
selectedIndex = selected,
onSelectionChange = { selected = it },
style = SegmentedControlStyle.iOS, // iOS | Material3 | Pill
)Disabled segments
SegmentedControl(
items = labels.mapIndexed { i, label -> SegmentItem(label, enabled = i != 2) },
selectedIndex = selected,
onSelectionChange = { selected = it }, // never fires for segment 2
)Equal vs content width
// Equal: each segment shares the width evenly; long labels ellipsize.
SegmentedControl(items, selected, { selected = it }, width = SegmentedControlWidth.Equal, modifier = Modifier.fillMaxWidth())
// Content: each segment is as wide as its content; the control grows to fit.
SegmentedControl(items, selected, { selected = it }, width = SegmentedControlWidth.Content)Icon-only
SegmentedControl(
items = listOf(
SegmentItem(icon = Icons.Default.FormatAlignLeft),
SegmentItem(icon = Icons.Default.FormatAlignCenter),
SegmentItem(icon = Icons.Default.FormatAlignRight),
),
selectedIndex = selected,
onSelectionChange = { selected = it },
)Keyboard navigation — on Desktop and Web, focus the control (Tab) and use the Left/Right arrow keys to move the selection between enabled segments. No extra setup required.
SegmentedControl(
items = items,
selectedIndex = selected,
onSelectionChange = { selected = it },
style = SegmentedControlStyle.Pill,
colors = SegmentedControlDefaults.colors(SegmentedControlStyle.Pill).copy(
thumbColor = MaterialTheme.colorScheme.tertiary,
selectedContentColor = MaterialTheme.colorScheme.onTertiary,
),
shape = RoundedCornerShape(8.dp),
animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy),
)style — iOS, Material3 or Pill. Drives the track/thumb shape, elevation and dividers.colors — start from SegmentedControlDefaults.colors(style) and .copy(...) what you need,
or build a SegmentedControlColors from scratch.shape — overrides the track and thumb shape.animationSpec — any AnimationSpec<Float>; defaults to a gentle spring.width — Equal or Content.enabled — false makes the whole control inert and muted.| SegmentedControl | Material 3 SingleChoiceSegmentedButtonRow
|
Hand-rolled Row
|
|
|---|---|---|---|
| iOS-native look | ✅ built-in iOS style |
❌ always Material | |
| Material look | ✅ Material3 style |
✅ | |
| Animated indicator | ✅ spring, configurable | ❌ DIY | |
| Icon / text / both | ✅ | ✅ | |
| Disabled segments | ✅ per-segment | ✅ | |
| Equal & content width | ✅ | ||
| Keyboard navigation | ✅ arrow keys | ❌ | ❌ DIY |
| RTL | ✅ | ✅ | |
| Multiplatform | ✅ Android/iOS/Desktop/Web | ✅ | ✅ |
Be honest with yourself: if you only ship Android and you're all-in on Material, the built-in M3 control is fine. Reach for this library when you want an iOS-appropriate look, a richer animated indicator, or keyboard support — without maintaining your own component.
See CONTRIBUTING.md. Bug reports and feature requests are welcome via GitHub Issues.
Copyright 2026 Nadeem Iqbal
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
See LICENSE for the full text.
An iOS-style segmented control for Compose Multiplatform — platform-appropriate visuals with three styles built in, an animated selection indicator, and full keyboard & RTL support.
Material 3's SingleChoiceSegmentedButtonRow always looks like Material — which feels out of place
on iOS, where users expect the familiar UISegmentedControl. SegmentedControl gives you a
platform-appropriate look from a single Compose codebase: ship the iOS style on Apple platforms,
Material3 on Android, or Pill everywhere — without forking your UI code.
| Platform | Supported | Tested |
|---|---|---|
| Android | ✅ | ✅ (unit + UI) |
| iOS | ✅ | ✅ (UI, Skiko) |
| Desktop | ✅ | ✅ (unit + UI) |
| Web | ✅ | ✅ (compile + logic) |
gradle/libs.versions.toml:
[libraries]
segmented-control = { module = "io.github.nadeemiqbal:segmented-control", version = "0.1.0" }commonMain dependencies:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.segmented.control)
}
}
}var selected by remember { mutableStateOf(0) }
SegmentedControl(
items = listOf("Day", "Week", "Month"),
selectedIndex = selected,
onSelectionChange = { selected = it },
modifier = Modifier.fillMaxWidth(),
)Icon + text segments
SegmentedControl(
items = listOf(
SegmentItem("List", Icons.Default.List),
SegmentItem("Grid", Icons.Default.GridView),
),
selectedIndex = selected,
onSelectionChange = { selected = it },
)Pick a style
SegmentedControl(
items = items,
selectedIndex = selected,
onSelectionChange = { selected = it },
style = SegmentedControlStyle.iOS, // iOS | Material3 | Pill
)Disabled segments
SegmentedControl(
items = labels.mapIndexed { i, label -> SegmentItem(label, enabled = i != 2) },
selectedIndex = selected,
onSelectionChange = { selected = it }, // never fires for segment 2
)Equal vs content width
// Equal: each segment shares the width evenly; long labels ellipsize.
SegmentedControl(items, selected, { selected = it }, width = SegmentedControlWidth.Equal, modifier = Modifier.fillMaxWidth())
// Content: each segment is as wide as its content; the control grows to fit.
SegmentedControl(items, selected, { selected = it }, width = SegmentedControlWidth.Content)Icon-only
SegmentedControl(
items = listOf(
SegmentItem(icon = Icons.Default.FormatAlignLeft),
SegmentItem(icon = Icons.Default.FormatAlignCenter),
SegmentItem(icon = Icons.Default.FormatAlignRight),
),
selectedIndex = selected,
onSelectionChange = { selected = it },
)Keyboard navigation — on Desktop and Web, focus the control (Tab) and use the Left/Right arrow keys to move the selection between enabled segments. No extra setup required.
SegmentedControl(
items = items,
selectedIndex = selected,
onSelectionChange = { selected = it },
style = SegmentedControlStyle.Pill,
colors = SegmentedControlDefaults.colors(SegmentedControlStyle.Pill).copy(
thumbColor = MaterialTheme.colorScheme.tertiary,
selectedContentColor = MaterialTheme.colorScheme.onTertiary,
),
shape = RoundedCornerShape(8.dp),
animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy),
)style — iOS, Material3 or Pill. Drives the track/thumb shape, elevation and dividers.colors — start from SegmentedControlDefaults.colors(style) and .copy(...) what you need,
or build a SegmentedControlColors from scratch.shape — overrides the track and thumb shape.animationSpec — any AnimationSpec<Float>; defaults to a gentle spring.width — Equal or Content.enabled — false makes the whole control inert and muted.| SegmentedControl | Material 3 SingleChoiceSegmentedButtonRow
|
Hand-rolled Row
|
|
|---|---|---|---|
| iOS-native look | ✅ built-in iOS style |
❌ always Material | |
| Material look | ✅ Material3 style |
✅ | |
| Animated indicator | ✅ spring, configurable | ❌ DIY | |
| Icon / text / both | ✅ | ✅ | |
| Disabled segments | ✅ per-segment | ✅ | |
| Equal & content width | ✅ | ||
| Keyboard navigation | ✅ arrow keys | ❌ | ❌ DIY |
| RTL | ✅ | ✅ | |
| Multiplatform | ✅ Android/iOS/Desktop/Web | ✅ | ✅ |
Be honest with yourself: if you only ship Android and you're all-in on Material, the built-in M3 control is fine. Reach for this library when you want an iOS-appropriate look, a richer animated indicator, or keyboard support — without maintaining your own component.
See CONTRIBUTING.md. Bug reports and feature requests are welcome via GitHub Issues.
Copyright 2026 Nadeem Iqbal
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
See LICENSE for the full text.