ComposeNavMotion is a reusable open-source library for animated Jetpack Compose navigation. It builds on Compose Animation and Navigation Compose with type-safe routes, global and per-screen animation control, Material motion presets, shared-element transitions, nested graphs, and accessibility-aware defaults.
Report a bug · Contributing · License
- Preview
- Features
- Installation
- Basic usage
- Type-safe routes
- Global default animation
- Per-screen override
- Nested graphs
- Material motion presets
- Shared elements
- Accessibility
- Sample app
- Project structure
- Contributing
- License
| Feature | Description |
|---|---|
| AnimatedNavHost | Host-level default animation and accessibility config |
| Type-safe routes | @Serializable routes with animatedComposable<T> |
| Animation resolution | Per-screen → nested graph → global default → fade |
| Preset transitions | fade, slideLeft, slideRight, slideUp, scale, none |
| Custom builder | Compose transitions with shared duration and easing |
| Direction-aware | directionAware() for forward push / backward pop |
| Material motion | MaterialNavMotion shared axis, fade through, container transform, modal |
| Shared elements | SharedNavElement via Compose SharedTransitionLayout |
| Nested graphs | animatedNavigation<T> with graph-level animation |
| Accessibility | Disable animations or respect system animator duration scale |
| Backward compatible | MVP 1–4 string-route NavAnimation API still works |
The demo above (400×889) was recorded from :navmotion-sample and shows:
- Home hub — presets, Material motion, and MVP 5 entry points
- List screen — type-safe route navigation
- Details — per-screen
containerTransformoverride - Animation selector — switch global default animation at runtime
- Shared elements —
SharedNavElementprofile avatar transitions - Nested graph —
animatedNavigation<MainGraph>with graph-level animation - Direction-aware checkout —
slideLeftforward /slideRightback - Profile, Settings, Modal, Sheet — custom and Material presets
dependencies {
implementation("io.github.saadkhalidkhan:composenavmotion:1.1.0")
}The published :nav-animation artifact re-exports :navmotion-core, :navmotion-material, and :navmotion-shared.
implementation(project(":navmotion-core"))
implementation(project(":navmotion-material")) // Material presets
implementation(project(":navmotion-shared")) // SharedNavElementApply the Kotlin Serialization plugin in your app module for type-safe routes.
import com.composenavmotion.AnimatedNavHost
import com.composenavmotion.animatedComposable
import com.composenavmotion.material.MaterialNavMotion
import kotlinx.serialization.Serializable
@Serializable object Home
@Serializable data class Details(val id: String)
AnimatedNavHost(
navController = navController,
startDestination = Home,
defaultAnimation = MaterialNavMotion.sharedAxisX(),
) {
animatedComposable<Home> {
HomeScreen()
}
animatedComposable<Details>(
animation = MaterialNavMotion.containerTransform(),
) { entry ->
val details = entry.toRoute<Details>()
DetailsScreen(details.id)
}
}Annotate routes with @Serializable and register them with reified animatedComposable:
@Serializable object Home
@Serializable data class Details(val id: String)
animatedComposable<Home> { HomeScreen() }
animatedComposable<Details> { entry ->
val args = entry.toRoute<Details>()
DetailsScreen(args.id)
}Navigate with route instances:
navController.navigate(Details("item-42"))Set one animation for the entire host:
AnimatedNavHost(
navController = navController,
startDestination = Home,
defaultAnimation = MaterialNavMotion.sharedAxisX(),
) {
animatedComposable<Home> { HomeScreen() }
animatedComposable<Settings> { SettingsScreen() } // inherits sharedAxisX
}Pass animation on a single destination to override the global default:
animatedComposable<Details>(
animation = MaterialNavMotion.containerTransform(),
) { entry ->
DetailsScreen(entry.toRoute<Details>().id)
}Resolution priority: per-screen → nested graph → global default → NavMotion.fade().
@Serializable object MainGraph
@Serializable object NestedHome
@Serializable data class NestedDetails(val id: String)
AnimatedNavHost(navController, startDestination = Home) {
animatedNavigation<MainGraph>(
startDestination = NestedHome,
animation = MaterialNavMotion.sharedAxisY(),
) {
animatedComposable<NestedHome> { NestedHomeScreen() }
animatedComposable<NestedDetails> { entry ->
NestedDetailsScreen(entry.toRoute<NestedDetails>().id)
}
}
}Destinations inside the nested graph inherit the graph animation unless they override it.
import com.composenavmotion.material.MaterialNavMotion
MaterialNavMotion.sharedAxisX()
MaterialNavMotion.sharedAxisY()
MaterialNavMotion.fadeThrough()
MaterialNavMotion.containerTransform()
MaterialNavMotion.modal()| Preset | Use case |
|---|---|
sharedAxisX() |
Lateral peer navigation |
sharedAxisY() |
Vertical parent/child flows |
fadeThrough() |
Unrelated destinations at the same level |
containerTransform() |
Detail expansion style (scale + fade) |
modal() |
Bottom sheet / modal presentation |
Wrap matching content on source and target screens with the same key:
import com.composenavmotion.shared.AnimatedNavHostWithSharedTransitions
import com.composenavmotion.shared.SharedNavElement
AnimatedNavHostWithSharedTransitions(
navController = navController,
startDestination = Home,
) {
animatedComposable<ProfileList> {
SharedNavElement(key = "profile-image-$userId") {
ProfileAvatar(userId)
}
}
animatedComposable<ProfileDetail> { entry ->
val id = entry.toRoute<ProfileDetail>().userId
SharedNavElement(key = "profile-image-$id") {
ProfileAvatar(id, large = true)
}
}
}Uses Compose ExperimentalSharedTransitionApi (SharedTransitionLayout + Modifier.sharedElement). When shared transitions are unavailable, SharedNavElement renders content without shared-element behavior.
AnimatedNavHost(
navController = navController,
startDestination = Home,
animationsEnabled = false,
) { /* ... */ }Or with full config:
AnimatedNavHost(
navController = navController,
startDestination = Home,
config = NavMotionConfig(
animationsEnabled = true,
respectSystemAnimatorScale = true,
defaultDuration = 300,
),
) { /* ... */ }When animations are disabled, all destinations use NavMotion.none() (EnterTransition.None / ExitTransition.None).
respectSystemAnimatorScale disables transitions when the system Animator duration scale is 0 (Remove animations accessibility setting).
./gradlew :navmotion-sample:installDebugThe sample demonstrates:
- Home — hub for all demos
- List screen — browse items with type-safe routes
- Details — per-screen
containerTransformoverride - Profile — classic custom slide preset
- Settings & Modal — Material presets
- Sheet — custom mixed slide/fade
- Animation selector — change global default at runtime
- Shared elements —
SharedNavElementprofile avatars - Nested graph —
animatedNavigation<MainGraph> - Direction-aware checkout —
NavMotion.directionAware() - MVP 1–4 compatibility —
NavAnimationstring-route API
| Module | Description |
|---|---|
:navmotion-core |
AnimatedNavHost, NavMotion, type-safe routes, config |
:navmotion-material |
MaterialNavMotion presets |
:navmotion-shared |
SharedNavElement, AnimatedNavHostWithSharedTransitions |
:nav-animation |
Published aggregator (Maven Central) |
:navmotion-sample |
Demo application |
The original API remains available:
import com.composenavmotion.NavAnimation
import com.composenavmotion.animatedComposable
NavHost(navController, startDestination = "home") {
animatedComposable(route = "details", animation = NavAnimation.slideLeft()) {
DetailsScreen()
}
}NavAnimation and NavAnimationSpec are deprecated aliases for NavMotion and NavMotionSpec.
Run tests and assemble the sample before opening a PR:
./gradlew :navmotion-core:testDebugUnitTest :navmotion-material:testDebugUnitTest :navmotion-shared:testDebugUnitTest :navmotion-sample:assembleDebugSee CONTRIBUTING.md.
Apache License 2.0 — see LICENSE.
Copyright 2026 Saad Khan
Saad Khan — GitHub · ranasaad0799@gmail.com
