🎨 FlowTab
🎬 Preview
In Action
| Light Theme | Dark Theme |
|---|
 |  |
Smooth animations, expandable search, and glassmorphism effects
✨ Features
📦 Installation
Gradle (Kotlin DSL) - Recommended
Add to your libs.versions.toml:
[versions]
flowtab-cmp = "0.5.6-beta"
Then in your module's build.gradle.kts:
dependencies {
implementation(libs.flowtab.cmp)
}
Gradle (Groovy)
dependencies {
implementation 'io.github.alims-repo:flowtab-cmp:0.5.6-beta'
}
🚀 Quick Start
Here's a minimal example to get you started:
That's it! FlowTab handles the UI and animations while you control the navigation logic.
📚 Core Concepts
Philosophy
FlowTab follows a presentation-only architecture. It manages visual state and animations but delegates all navigation decisions to you through simple callbacks:
BottomNavigation(
items = navItems,
selectedId = selectedId,
onItemSelected = { item ->
selectedId = item.id
}
)
This design makes FlowTab compatible with any navigation solution or state management approach.
🎯 Integration Examples
With Navigation Compose (androidx.navigation)
@Composable
fun {
navController = rememberNavController()
navBackStackEntry navController.currentBackStackEntryAsState()
currentRoute = navBackStackEntry?.destination?.route ?:
Scaffold(
bottomBar = {
BottomNavigation(
items = navItems,
selectedId = currentRoute,
onItemSelected = { item ->
navController.navigate(item.id) {
popUpTo(navController.graph.findStartDestination().id) {
saveState =
}
launchSingleTop =
restoreState =
}
}
)
}
) { padding ->
NavHost(
navController = navController,
startDestination = ,
modifier = Modifier.padding(padding)
) {
composable() { HomeScreen() }
composable() { SearchScreen() }
composable() { ProfileScreen() }
}
}
}
With Decompose
With Voyager
object HomeTab : Tab {
override val options: TabOptions
@Composable get() = TabOptions(index = 0u, title = "Home")
{ HomeScreen() }
}
{
tabs = remember { listOf(HomeTab, SearchTab, ProfileTab) }
TabNavigator(tab = HomeTab) { tabNavigator ->
Scaffold(
bottomBar = {
BottomNavigation(
items = navItems,
selectedId = tabs.indexOf(tabNavigator.current).toString(),
onItemSelected = { item ->
tabNavigator.current = tabs[item.id.toInt()]
}
)
}
) { padding ->
CurrentTab(modifier = Modifier.padding(padding))
}
}
}
🎨 Customization
Selection Indicators
Customize how selected items are indicated with three different styles:
Ripple Indicator (Default)
A full-width background highlight that fills behind the selected item:
BottomNavigation(
items = navItems,
selectedId = selectedId,
onItemSelected = { item -> selectedId = item.id },
config = NavConfig(
navIndicator = NavIndicator.Ripple(
color = MaterialTheme.colorScheme.primaryContainer,
indicatorPadding = 4.dp
)
)
)
Dot Indicator
A small circular indicator below the selected item:
BottomNavigation(
items = navItems,
selectedId = selectedId,
onItemSelected = { item -> selectedId = item.id },
config = NavConfig(
navIndicator = NavIndicator.Dot(
size = 8.dp,
color = MaterialTheme.colorScheme.primary,
indicatorPadding = 4.dp
)
)
)
Line Indicator
A horizontal line below the selected item:
BottomNavigation(
items = navItems,
selectedId = selectedId,
onItemSelected = { item -> selectedId = item.id },
config = NavConfig(
navIndicator = NavIndicator.Line(
height = 3.dp,
width = 40.dp,
color = MaterialTheme.colorScheme.primary,
indicatorPadding = 4.dp
)
)
)
Indicator Comparison:
- Ripple: Best for bold, high-contrast designs. Fills the entire item background.
- Dot: Minimal and modern. Perfect for clean, Instagram-style navigation.
- Line: Material Design 3 style. Subtle yet clear indication.
Badges
Add notification counts or dot indicators:
NavItem(
id = "notifications",
label = "Notifications",
icon = Icons.Outlined.Notifications,
selectedIcon = Icons.Filled.Notifications,
badge = BadgeData(count = 5)
)
NavItem(
id = "messages",
label = "Messages",
icon = Icons.Outlined.Message,
badge = BadgeData(showDot = true)
)
Search Bar
Create an expandable search experience:
val navItems = listOf(
NavItem(id = "home", label = "Home", icon = Icons.Default.Home),
NavItem(
id = "search",
label = "Search",
icon = Icons.Default.Search,
type = NavItemType.Search
),
NavItem(id = "profile", label = "Profile", icon = Icons.Default.Person)
)
BottomNavigation(
items = navItems,
selectedId = selectedId,
onItemSelected = { item -> selectedId = item.id },
onQueryChange = { query ->
searchViewModel.updateQuery(query)
},
onSearch = { query ->
searchViewModel.performSearch(query)
}
)
Isolated Items (FAB-like)
Add special action buttons that don't participate in navigation selection:
NavItem(
id = "add",
label = "Add",
icon = Icons.Default.Add,
type = NavItemType.Isolated(rotation = 45f)
)
Styling Presets
Instagram-Style
val instagramConfig = NavConfig(
height = 50.dp,
cornerRadius = 0.dp,
showLabels = false,
enableBlur = false,
showBorder = false,
navColor = NavColor(
backgroundColor = Color.Black,
selectedIconColor = Color.White,
unSelectedIconColor = Color.Gray
),
navIndicator = NavIndicator.Dot(
size = 6.dp,
color = Color.White
)
)
Modern Pill Style
val pillConfig = NavConfig(
height = 60.dp,
cornerRadius = 60.dp,
maxWidth = 400.dp,
enableBlur = true,
blurIntensity = 0.95f,
showBorder = true,
elevation = 8.dp,
navIndicator = NavIndicator.Ripple(
color = MaterialTheme.colorScheme.primaryContainer
)
)
Floating Minimal
val floatingConfig = NavConfig(
height = 56.dp,
cornerRadius = 28.dp,
showLabels = false,
elevation = 12.dp,
maxWidth = 320.dp,
navIndicator = NavIndicator.Line(
height = 2.dp,
width = 40.dp,
color = MaterialTheme.colorScheme.primary
)
)
Glassmorphism with Haze
Create beautiful blur effects over scrollable content:
val hazeState = rememberHazeState()
Scaffold(
bottomBar = {
BottomNavigation(
items = navItems,
selectedId = selectedId,
onItemSelected = { },
hazeState = hazeState,
config = NavConfig(
enableBlur = true,
blurIntensity = 0.95f
)
)
}
) { padding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.hazeSource(state = hazeState)
) {
items(100) { index ->
Text("Item $index")
}
}
}
Custom Colors
val customColors = NavColor(
backgroundColor = Color(0xFF1E1E1E),
borderColor = Color(0xFF3A3A3A),
selectedIconColor = Color(0xFF00D9FF),
unSelectedIconColor = Color(0xFF666666),
selectedTextColor = Color(0xFF00D9FF),
unSelectedTextColor = Color(0xFF999999),
selectedRippleColor = Color(0x3300D9FF)
)
BottomNavigation(
items = navItems,
selectedId = selectedId,
onItemSelected = { },
config = NavConfig(navColor = customColors)
)
🎭 Item Types
FlowTab supports three item types:
sealed class NavItemType {
data object Standard : NavItemType()
data object Search : NavItemType()
data class Isolated(val rotation: Float = ) : NavItemType()
}
🎯 Indicator Types
FlowTab offers three selection indicator styles:
📖 Configuration Reference
NavConfig Parameters
💡 Best Practices
✅ Do
val navItems = remember {
listOf(
NavItem(id = "home", label = "Home", icon = Icons.Default.Home),
NavItem(id = "search", label = "Search", icon = Icons.Default.Search)
)
}
NavItem(id = , label = , icon = Icons.Default.Person)
NavItem(id = , label = , icon = Icons.Default.Settings)
onQueryChange = { query -> viewModel.updateSearchQuery(query) }
onSearch = { query -> viewModel.performSearch(query) }
navIndicator = NavIndicator.Line()
navIndicator = NavIndicator.Dot()
navIndicator = NavIndicator.Ripple()
❌ Don't
val navItems = listOf(NavItem(...))
NavItem(id = "1", ...)
NavItem(id = "screen", ...)
onQueryChange = { query -> performExpensiveSearch(query) }
🧪 Testing
Example unit test:
@Test
fun `bottom navigation handles item selection correctly`() {
var selectedId = "home"
composeTestRule.setContent {
BottomNavigation(
items = listOf(
NavItem(id = "home", label = "Home", icon = Icons.Default.Home),
NavItem(id = "profile", label = "Profile", icon = Icons.Default.Person)
),
selectedId = selectedId,
onItemSelected = { item -> selectedId = item.id }
)
}
composeTestRule.onNodeWithText("Profile").performClick()
assertEquals("profile", selectedId)
}
🤝 Contributing
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature)
- Commit your changes (
git commit -m 'Add amazing feature')
- Push to the branch (
git push origin feature/amazing-feature)
- Open a Pull Request
Please read our Contributing Guide for more details.
📄 License
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Copyright 2026 Alim
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:
Unless applicable law agreed to writing, software
distributed under the License distributed an BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express implied.
See the License the specific language governing permissions
limitations under the License.
🙏 Acknowledgments
- Inspired by modern mobile design patterns and Material Design 3
- Built with Jetpack Compose and Compose Multiplatform
- Blur effects powered by Haze by Chris Banes
- Icons from Material Icons Extended
📞 Support & Community
🌟 Showcase
Using FlowTab in your app? We'd love to feature it! Open an issue with the showcase label.