
Enables Instagram-style tab navigation with intuitive back navigation and unique tab stack management. Each tab maintains its own back stack, enhancing user experience with familiar, efficient navigation.
A Kotlin Multiplatform library that mimics the tab navigation behaviour seen in apps like Instagram and YouTube.
This behaviour is particularly noticeable on Android, where the back button allows users to navigate across previously visited tabs in reverse order — a pattern not native to iOS.
For those unfamiliar: Instagram’s tab navigation remembers the order in which tabs were visited. When pressing "back", it navigates to the last visited tab. Each tab is also kept uniquely in the tab stack — it's never added twice.
The first tab (home tab) is treated specially: it's reinserted if popped earlier, ensuring the app always exits from the home tab.
This creates a smooth and intuitive experience, where users move across tabs naturally and return to the home screen before the app closes.
https://github.com/user-attachments/assets/d53144c4-ba82-4419-8a04-b52901e65a59
Add the dependency to your project:
implementation("dev.kadoglou:instanav.core:1.0.0")Define your tabs and navigation state:
val tabs = listOf(
InstaTab("tab_1", ScreenA.route),
InstaTab("tab_2", ScreenB.route),
)
val instaNavState = rememberInstaNavState(
initialTabs = listOf("tab_1") // The first tab is treated as the "home tab"
)
val controllers = List(tabs.size) { rememberNavController() }
val tabNavControllers = remember(controllers) {
tabs.mapIndexed { i, tab -> tab.tabRoute to controllers[i] }.toMap()
}Replace your normal NavHost with InstaNavHost and pass in the state, tabs, and controllers.
InstaNavHost(
state = instaNavState,
tabs = tabs,
tabNavControllers = tabNavControllers
) {
composable(ScreenA.route) { ScreenA().context() }
composable(ScreenB.route) { ScreenB().context() }
// You can also use custom extensions like animatableComposable
animatableComposable(SubScreenA.route) { SubScreenA().context() }
}Switch tabs with:
instaNavState.navigateTo("tab_2")This will:
To go "back" between visited tabs:
instaNavState.popBackTabStack()You can embed this into your Scaffold and use something like a BottomBar to control navigation.
The LocalInstaNavController is used to expose the active NavHostController to your screens.
CompositionLocalProvider(
LocalInstaNavController provides tabNavControllers.getValue(instaNavState.currentTab)
) {
Scaffold(
bottomBar = {
BottomBar(
currentTab = instaNavState.currentTab,
tabs = tabs
) { selectedTab ->
instaNavState.navigateTo(selectedTab)
}
}
) { padding ->
Box(Modifier.padding(padding)) {
InstaNavHost(
state = instaNavState,
tabs = tabs,
tabNavControllers = tabNavControllers
) {
composable(ScreenA.route) { ScreenA().context() }
composable(ScreenB.route) { ScreenB().context() }
animatableComposable(SubScreenA.route) { SubScreenA().context() }
}
}
}
}To persist tab history across process death:
val instaNavState = rememberInstaNavState(
initialTabs = listOf("tab_1"),
saveTab = { it }, // Convert to String
restoreTab = { it } // Restore from String
)CompositionLocalProvider to expose the active tab's NavHostController:CompositionLocalProvider(
LocalInstaNavController provides tabNavControllers.getValue(instaNavState.currentTab)
) {
// Screens can now use LocalInstaNavController.current
}openTabs), helping reduce memory usage.A Kotlin Multiplatform library that mimics the tab navigation behaviour seen in apps like Instagram and YouTube.
This behaviour is particularly noticeable on Android, where the back button allows users to navigate across previously visited tabs in reverse order — a pattern not native to iOS.
For those unfamiliar: Instagram’s tab navigation remembers the order in which tabs were visited. When pressing "back", it navigates to the last visited tab. Each tab is also kept uniquely in the tab stack — it's never added twice.
The first tab (home tab) is treated specially: it's reinserted if popped earlier, ensuring the app always exits from the home tab.
This creates a smooth and intuitive experience, where users move across tabs naturally and return to the home screen before the app closes.
https://github.com/user-attachments/assets/d53144c4-ba82-4419-8a04-b52901e65a59
Add the dependency to your project:
implementation("dev.kadoglou:instanav.core:1.0.0")Define your tabs and navigation state:
val tabs = listOf(
InstaTab("tab_1", ScreenA.route),
InstaTab("tab_2", ScreenB.route),
)
val instaNavState = rememberInstaNavState(
initialTabs = listOf("tab_1") // The first tab is treated as the "home tab"
)
val controllers = List(tabs.size) { rememberNavController() }
val tabNavControllers = remember(controllers) {
tabs.mapIndexed { i, tab -> tab.tabRoute to controllers[i] }.toMap()
}Replace your normal NavHost with InstaNavHost and pass in the state, tabs, and controllers.
InstaNavHost(
state = instaNavState,
tabs = tabs,
tabNavControllers = tabNavControllers
) {
composable(ScreenA.route) { ScreenA().context() }
composable(ScreenB.route) { ScreenB().context() }
// You can also use custom extensions like animatableComposable
animatableComposable(SubScreenA.route) { SubScreenA().context() }
}Switch tabs with:
instaNavState.navigateTo("tab_2")This will:
To go "back" between visited tabs:
instaNavState.popBackTabStack()You can embed this into your Scaffold and use something like a BottomBar to control navigation.
The LocalInstaNavController is used to expose the active NavHostController to your screens.
CompositionLocalProvider(
LocalInstaNavController provides tabNavControllers.getValue(instaNavState.currentTab)
) {
Scaffold(
bottomBar = {
BottomBar(
currentTab = instaNavState.currentTab,
tabs = tabs
) { selectedTab ->
instaNavState.navigateTo(selectedTab)
}
}
) { padding ->
Box(Modifier.padding(padding)) {
InstaNavHost(
state = instaNavState,
tabs = tabs,
tabNavControllers = tabNavControllers
) {
composable(ScreenA.route) { ScreenA().context() }
composable(ScreenB.route) { ScreenB().context() }
animatableComposable(SubScreenA.route) { SubScreenA().context() }
}
}
}
}To persist tab history across process death:
val instaNavState = rememberInstaNavState(
initialTabs = listOf("tab_1"),
saveTab = { it }, // Convert to String
restoreTab = { it } // Restore from String
)CompositionLocalProvider to expose the active tab's NavHostController:CompositionLocalProvider(
LocalInstaNavController provides tabNavControllers.getValue(instaNavState.currentTab)
) {
// Screens can now use LocalInstaNavController.current
}openTabs), helping reduce memory usage.