import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.composeunstyled.Tab
import com.composeunstyled.TabList
import com.composeunstyled.TabPanel
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledTabGroup
@Composable
fun TabGroupDemo() {
class Article(val title: String, val relativeTime: String, val comments: Int, val points: Int)
val categories = mapOf<String, List<Article>>(
"Trending" to listOf(
Article(
title = "I hosted my startup's backend on a Tamagotchi – AMA",
relativeTime = "11 hours ago",
comments = 312,
points = 1042,
),
Article(
title = "I fired myself to improve company culture — it worked",
relativeTime = "9 hours ago",
comments = 264,
points = 928,
),
),
"Latest" to listOf(
Article(
title = "The office microwave is now a Kubernetes node",
relativeTime = "2 hours ago",
comments = 87,
points = 356,
),
Article(
title = "We replaced scrum with interpretive dancing",
relativeTime = "1 hour ago",
comments = 52,
points = 198,
),
),
"Popular" to listOf(
Article(
title = "Social network for ants is growing fast",
relativeTime = "14 hours ago",
comments = 412,
points = 1376,
),
Article(
title = "Why I quit my $800K FAANG job to grow mushrooms",
relativeTime = "16 hours ago",
comments = 391,
points = 1204,
),
),
)
var selectedTab by remember { mutableStateOf(categories.keys.first()) }
Box(
modifier = Modifier.fillMaxSize()
.padding(16.dp)
.padding(top = 90.dp),
contentAlignment = Alignment.TopCenter,
) {
UnstyledTabGroup(
selectedTab = selectedTab,
onSelectedTabChange = { selectedTab = it },
tabs = categories.keys.toList(),
modifier = Modifier.widthIn(max = 450.dp),
) {
Column {
TabList(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFFF8FAFC))
.border(1.dp, Color(0xFFCACACA), RoundedCornerShape(8.dp)),
) {
Row(Modifier.fillMaxSize()) {
categories.forEach { (key, _) ->
Tab(
key = key,
modifier = Modifier.weight(1f).fillMaxHeight(),
indication = LocalIndication.current,
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
BasicText(
text = key,
style = TextStyle(
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
color = if (selected) {
Color.Black
} else {
Color(0xFF757575)
},
),
)
if (selected) {
Box(
modifier = Modifier
.background(
color = Color.Black,
shape = RoundedCornerShape(2.dp),
)
.fillMaxWidth()
.height(3.dp)
.align(Alignment.BottomCenter),
)
}
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
categories.forEach { (key, items) ->
TabPanel(
key = key,
modifier = Modifier
.fillMaxWidth()
.background(
color = Color(0xFFF8FAFC),
shape = RoundedCornerShape(8.dp),
)
.border(1.dp, Color(0xFFCACACA), RoundedCornerShape(8.dp)),
) {
Column(Modifier.padding(16.dp)) {
items.forEach { item ->
UnstyledButton(
onClick = { /* TODO */ },
modifier = Modifier.clip(RoundedCornerShape(8.dp)),
indication = LocalIndication.current,
) {
Column(Modifier.padding(12.dp)) {
BasicText(item.title, style = TextStyle(fontWeight = FontWeight.Medium))
Spacer(Modifier.height(4.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().alpha(0.6f),
) {
BasicText(item.relativeTime)
BasicText("·")
BasicText("${item.comments} comments")
BasicText("·")
BasicText("${item.points} shares")
}
}
}
}
}
}
}
}
}
}
}Features
- Generic tab keys
- Horizontal and vertical tab lists
- Automatic or manual activation
- Focus handoff to panels
Installation
implementation("com.composables:composeunstyled-tab-group")
Anatomy
val tabs = listOf("account", "billing")
UnstyledTabGroup(
selectedTab = selectedTab,
onSelectedTabChange = onSelectedTabChange,
tabs = tabs,
) {
TabList {
tabs.forEach { tab ->
Tab(tab) {
}
}
}
tabs.forEach { tab ->
TabPanel(tab) {
}
}
}
Concepts
UnstyledTabGrouprepresents a set of tabs and panels grouped by key.TabListrenders the group of tabs.Tabrenders one tab insideTabList.TabPanelrenders the panel for the selected tab key.
Accessibility
Keep tabs in the same order as the visual tabs. TabList uses that order for arrow-key navigation, Home, and End.
Code Examples
Selecting tabs manually
Use the selectedTab parameter to control the active tab:
val tabs = listOf("account", "billing")
var selectedTab by remember { mutableStateOf("account") }
UnstyledTabGroup(
selectedTab = selectedTab,
onSelectedTabChange = { selectedTab = it },
tabs = tabs,
) {
TabList {
tabs.forEach { tab ->
Tab(tab) {
BasicText(tab)
}
}
}
TabPanel("account") {
BasicText("Account")
}
TabPanel("billing") {
BasicText("Billing")
}
}
Creating vertical tabs
Use the orientation parameter on TabList to change arrow-key navigation for vertical tabs:
TabList(orientation = Orientation.Vertical) {
tabs.forEach { tab ->
Tab(tab) {
BasicText(tab)
}
}
}
Requiring click activation
Use the activateOnFocus parameter when arrow-key focus should not select tabs:
Tab("billing", activateOnFocus = false) {
BasicText("Billing")
}
Disabling a tab
Use the enabled parameter to keep a tab visible but unavailable:
Tab(
key = "billing",
enabled = false,
) {
BasicText("Billing")
}
API Reference
UnstyledTabGroup
| Parameter | Type | Description |
|---|---|---|
selectedTab |
T |
The initial selected tab for the tab group state. |
onSelectedTabChange |
(T) -> Unit |
|
tabs |
List<T> |
|
modifier |
Modifier |
Modifier to be applied to the tab. |
content |
TabGroupScope<T>.() -> Unit |
Composable function to define the content of the tab panel. |
TabGroupScope.TabList
| Parameter | Type | Description |
|---|---|---|
modifier |
Modifier |
Modifier to be applied to the tab. |
orientation |
Orientation |
The orientation of the tab list (horizontal or vertical). |
content |
TabListScope<T>.() -> Unit |
Composable function to define the content of the tab panel. |
TabListScope.Tab
| Parameter | Type | Description |
|---|---|---|
key |
T |
The unique key for the tab panel. |
modifier |
Modifier |
Modifier to be applied to the tab. |
enabled |
Boolean |
Whether the tab is enabled. |
activateOnFocus |
Boolean |
Whether to activate a tab when it receives focus. |
indication |
Indication? |
Visual indication for interactions. |
interactionSource |
MutableInteractionSource? |
Interaction source for the tab. |
content |
TabScope.() -> Unit |
Composable function to define the content of the tab panel. |
TabGroupScope.TabPanel
| Parameter | Type | Description |
|---|---|---|
key |
T |
The unique key for the tab panel. |
modifier |
Modifier |
Modifier to be applied to the tab. |
content |
() -> Unit |
Composable function to define the content of the tab panel. |