DEV Community

HarmonyOS
HarmonyOS

Posted on

ArkUI “Shared Element Transition” (Hero-style Animations & Flutter Hero)

Read the original article:ArkUI “Shared Element Transition” (Hero-style Animations & Flutter Hero)

Context

Flutter’s Hero animates the same visual element between two screens. In ArkTS/ArkUI you achieve the same effect with two patterns:

  • Across pages (Router): sharedTransition(id, options)
  • Within a single scene / Navigation: geometryTransition(id) + getUIContext().animateTo(...)

Description

Goal: when going from a list (small thumbnail) to a detail view (large artwork), smoothly animate the same content between two UI states. In ArkUI, when two elements share the same id, their geometry (position/size) is animated; you can add timing curves and secondary effects like opacity.

Solution / Approach

1) Cross-page Hero with Router — sharedTransition

Mark the element on both pages with the same id; ArkUI drives the shared-element transition automatically.

ListPage.ets

import router from '@ohos.router';

@Entry
@Component
struct ListPage {
  private items: number[] = Array.from({ length: 12 }, (_, i) => i);

  build() {
    Grid() {
      ForEach(this.items, (idx: number) => {
        GridItem() {
          Image($r('app.media.ic_sample'))
            .width(80).height(80).borderRadius(12)
            .sharedTransition(`hero-${idx}`, { duration: 350, curve: Curve.EaseInOut })
            .onClick(() => router.pushUrl({ url: 'pages/DetailPage', params: { id: idx } }))
        }
      }, i => i.toString())
    }
    .columnsTemplate('1fr 1fr 1fr')
    .columnsGap(12).rowsGap(12)
    .padding(16)
  }
}
Enter fullscreen mode Exit fullscreen mode

DetailPage.ets

import router from '@ohos.router';

@Entry
@Component
struct DetailPage {
  @State id: number = 0;

  aboutToAppear() {
    const p = router.getParams() as Record<string, number>;
    this.id = p?.id ?? 0;
  }

  build() {
    Column({ space: 16 }) {
      Image($r('app.media.ic_sample'))
        .width('90%').aspectRatio(1).borderRadius(24)
        .sharedTransition(`hero-${this.id}`, { duration: 350, curve: Curve.EaseInOut })

      Button('Back').onClick(() => router.back())
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
  }
}
Enter fullscreen mode Exit fullscreen mode

Tips

  • The same id must be used on both pages (e.g., hero-7hero-7).
  • options supports duration, curve, and optional delay, type, zIndex, motionPath.
  • You can also define overall page transitions via pageTransition—it works alongside the shared element.

2) Single-scene / Navigator Hero — geometryTransition + animateTo

Bind two elements with the same id and wrap the state change inside animateTo to get a fluid transition.

@Entry
@Component
struct GalleryHero {
  @State showDetail: boolean = false
  @State selected: number = -1
  private items: number[] = Array.from({ length: 9 }, (_, i) => i)

  build() {
    Stack({ alignContent: Alignment.Center }) {
      if (!this.showDetail) {
        Grid() {
          ForEach(this.items, (i: number) => {
            GridItem() {
              Image($r('app.media.ic_sample'))
                .width(80).height(80).borderRadius(12)
                .geometryTransition(`g-${i}`)
                .transition(TransitionEffect.Opacity) // subtle fade
                .onClick(() => {
                  getUIContext()?.animateTo({ duration: 350, curve: Curve.EaseInOut }, () => {
                    this.selected = i
                    this.showDetail = true
                  })
                })
            }
          }, i => i.toString())
        }
        .columnsTemplate('1fr 1fr 1fr')
        .columnsGap(12).rowsGap(12)
        .padding(16)
      } else {
        Column({ space: 16 }) {
          Image($r('app.media.ic_sample'))
            .width('88%').aspectRatio(1).borderRadius(24)
            .geometryTransition(`g-${this.selected}`)
            .transition(TransitionEffect.Opacity)

          Button('Back').onClick(() => {
            getUIContext()?.animateTo({ duration: 350, curve: Curve.EaseInOut }, () => {
              this.showDetail = false
            })
          })
        }
        .width('100%').height('100%')
        .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
      }
    }
    .width('100%').height('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode

Tips

  • Timing/curve come from animateTo (not from geometryTransition).
  • Keep visuals consistent (e.g., small borderRadius(12) → large borderRadius(24)); geometry changes read nicely during the transition.
  • Add a lightweight effect like TransitionEffect.Opacity to enhance perception.

Key Takeaways

  1. Two primary approaches: sharedTransition (Router, cross-page) and geometryTransition + animateTo (single scene/Navigation).
  2. A matching id on both ends is mandatory for the shared element.
  3. Timing/curve: via options on sharedTransition, or via animateTo for geometryTransition.
  4. Position/size animate smoothly; you can pair with fades or page transitions.
  5. Works great for list → detail flows on phones and wearables.
  • Keywords:

sharedTransition, geometryTransition, TransitionEffect, PageTransition, UIContext.animateTo

Additional Resources

https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-attribute-animation-apis-V14

https://docs.openharmony.cn/pages/v4.1/en/application-dev/ui/arkts-transition-overview.md

https://medium.com/huawei-developers/how-to-create-interactive-uis-with-animation-and-transition-effects-in-arkui-e0e5e0da3233

https://medium.com/%40arundroidkl92/mastering-shared-element-transitions-in-jetpack-compose-a-step-by-step-guide-27d4d5fec5a4

https://medium.com/simform-engineering/customisation-of-shared-element-transition-of-jetpack-compose-49eb0cba6f48

https://medium.com/huawei-developers/understanding-arkui-component-lifecycle-from-creation-to-destruction-4eb90a933168

https://gitee.com/openharmony/docs/blob/b43a89d679acd54fd3e613f8b3a91c283d7daa6c/en/application-dev/reference/apis-arkui/arkui-ts/ts-transition-animation-shared-elements.md

https://gitee.com/openharmony/docs/blob/master/en/application-dev/reference/apis-arkui/arkui-ts/ts-transition-animation-geometrytransition.md

https://gitee.com/openharmony/docs/blob/b43a89d679acd54fd3e613f8b3a91c283d7daa6c/en/application-dev/reference/apis-arkui/arkui-ts/ts-transition-animation-shared-elements.md

https://gitee.com/openharmony/docs/blob/73f242a3d024f6ee18c48b22242c2aa458ae3839/en/application-dev/reference/arkui-ts/ts-transition-animation-geometrytransition.md?skip_mobile=true

https://docs.openharmony.cn/pages/v4.1/en/application-dev/ui/arkts-shared-element-transition.md

https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-router-to-navigation

https://docs.openharmony.cn/pages/v4.1/en/application-dev/reference/apis-arkui/arkui-ts/ts-motion-path-animation.md

https://docs.openharmony.cn/pages/v4.1/en/application-dev/reference/apis-arkui/arkui-ts/ts-page-transition-animation.md

https://docs.flutter.dev/ui/animations/hero-animations

Written by Bunyamin Eymen Alagoz

Top comments (0)