Follow-up to #63, which brought both platforms onto a shared design system and added a motion layer (expressive motion scheme, animated chips and tabs, haptics, SF Symbol effects).
One piece of that motion work was deliberately left out, because it is a structural change rather than an additive one: the grid → detail transition. For a photo-first app this is the single highest-impact animation — the card the user tapped expanding into its detail view, instead of an unrelated sheet sliding up over it.
Why it wasn't done in #63
On both platforms it requires the item detail to become a pushed destination rather than a modal sheet:
- Android —
ModalBottomSheet renders in a separate platform window, and shared-element geometry does not resolve across windows. So no amount of tagging the card and the sheet will work; the detail content has to move into the same composition.
- iOS —
.navigationTransition(.zoom(sourceID:in:)) only applies to a NavigationStack push, not to a .sheet.
That is a change to how the detail screen is presented, not a change to how it animates, which is why it wanted its own PR.
What to do
Android
- Wrap the pager content in
SharedTransitionLayout.
- Tag the card photo in
ClothingCard.kt with Modifier.sharedElement(rememberSharedContentState(key = item.id), …).
- Render
ItemDetailContent as an in-composition destination via AnimatedContent instead of ModalBottomSheet. The content is already split out of the sheet wrapper (ItemDetailContent vs ItemDetailSheet), so this is mostly a hosting change.
compose.animation is currently only a transitive dependency — add it explicitly to composeApp/build.gradle.kts. SharedTransitionLayout is confirmed present in the resolved org.jetbrains.compose.animation:1.10.0.
iOS
.matchedTransitionSource(id:in:) on the card in ClothingCard.swift.
.navigationTransition(.zoom(sourceID:in:)) on the pushed detail.
- Wrap the wardrobe grid in a
NavigationStack (iOS 18+, inside the current 18.2 deployment floor — no availability gate needed).
Both
AddItemSheet and CreateOutfitSheet are forms and should stay modal sheets. Only the item detail changes. That leaves the two platforms structurally symmetric: detail = push, create/edit = sheet.
Constraints
journeys/outfit-item-grid.xml and journeys/add-first-item.xml both walk the item-detail flow, so they need re-running after this. item_detail_sheet, item_detail_edit and item_detail_delete must keep their identifiers even though the host changes.
- Back navigation has to keep working from the new destination, including predictive back on Android.
- Verify on a real device, not just the emulator — this is a motion change and frame pacing matters.
Notes
ExperimentalSharedTransitionApi and the material3 Expressive APIs this builds on are alpha. If the transition misbehaves, the fallback is to leave the detail as a ModalBottomSheet; everything in #63 stands on its own without this.
🤖 Generated with Claude Code
Follow-up to #63, which brought both platforms onto a shared design system and added a motion layer (expressive motion scheme, animated chips and tabs, haptics, SF Symbol effects).
One piece of that motion work was deliberately left out, because it is a structural change rather than an additive one: the grid → detail transition. For a photo-first app this is the single highest-impact animation — the card the user tapped expanding into its detail view, instead of an unrelated sheet sliding up over it.
Why it wasn't done in #63
On both platforms it requires the item detail to become a pushed destination rather than a modal sheet:
ModalBottomSheetrenders in a separate platform window, and shared-element geometry does not resolve across windows. So no amount of tagging the card and the sheet will work; the detail content has to move into the same composition..navigationTransition(.zoom(sourceID:in:))only applies to aNavigationStackpush, not to a.sheet.That is a change to how the detail screen is presented, not a change to how it animates, which is why it wanted its own PR.
What to do
Android
SharedTransitionLayout.ClothingCard.ktwithModifier.sharedElement(rememberSharedContentState(key = item.id), …).ItemDetailContentas an in-composition destination viaAnimatedContentinstead ofModalBottomSheet. The content is already split out of the sheet wrapper (ItemDetailContentvsItemDetailSheet), so this is mostly a hosting change.compose.animationis currently only a transitive dependency — add it explicitly tocomposeApp/build.gradle.kts.SharedTransitionLayoutis confirmed present in the resolvedorg.jetbrains.compose.animation:1.10.0.iOS
.matchedTransitionSource(id:in:)on the card inClothingCard.swift..navigationTransition(.zoom(sourceID:in:))on the pushed detail.NavigationStack(iOS 18+, inside the current 18.2 deployment floor — no availability gate needed).Both
AddItemSheetandCreateOutfitSheetare forms and should stay modal sheets. Only the item detail changes. That leaves the two platforms structurally symmetric: detail = push, create/edit = sheet.Constraints
journeys/outfit-item-grid.xmlandjourneys/add-first-item.xmlboth walk the item-detail flow, so they need re-running after this.item_detail_sheet,item_detail_editanditem_detail_deletemust keep their identifiers even though the host changes.Notes
ExperimentalSharedTransitionApiand the material3 Expressive APIs this builds on are alpha. If the transition misbehaves, the fallback is to leave the detail as aModalBottomSheet; everything in #63 stands on its own without this.🤖 Generated with Claude Code