Documentation

transition:crossfade

Create smooth shared element transitions between screens with matching keys.

Overview

Shared element transitions create visual continuity as you navigate between screens. An element on one screen morphs into the corresponding element on another screen.

Basic Usage

Add the same transition:crossfade key to elements on both screens:

List Screen

<LazyColumn>
  @for (movie in movies) {
    <Card onClick={() => $navigate("/movie/{movie.id}")}>
      <Image
        src={movie.poster}
        transition:crossfade="poster-{movie.id}"
      />
      <Text>{movie.title}</Text>
    </Card>
  }
</LazyColumn>

Detail Screen

<Column>
  <Image
    src={$data.posterUrl}
    transition:crossfade="poster-{$route.params.id}"
  />
  <Text>{$data.title}</Text>
  <Text>{$data.description}</Text>
</Column>

The crossfade key must be identical on both screens. Use dynamic values like IDs to ensure each item has a unique, matching key.

Configuration

Customize the transition with an options object:

<Image
  transition:crossfade={{
    key: "poster-{id}",
    duration: 400,
    type: "bounds"
  }}
/>

Options

OptionTypeDefaultDescription
keyStringRequiredUnique identifier to match elements
typeString"element""element" or "bounds" - animation style
durationNumber300Animation duration in milliseconds

Transition Types

  • "element" - Animates the element itself (recommended for images, icons)
  • "bounds" - Animates only position and size (recommended for containers)

Asymmetric Transitions

Use different durations for enter and exit:

<Image
  in:crossfade={{ key: "poster-{id}", duration: 400 }}
  out:crossfade={{ key: "poster-{id}", duration: 150 }}
/>

Examples

Product Gallery

// Products list
<LazyVerticalGrid columns={2}>
  @for (product in products) {
    <Card onClick={() => $navigate("/product/{product.id}")}>
      <Image
        src={product.image}
        transition:crossfade="product-{product.id}"
      />
      <Text>{product.name}</Text>
    </Card>
  }
</LazyVerticalGrid>

// Product detail
<Column>
  <Image
    src={$data.image}
    transition:crossfade="product-{$route.params.id}"
    h={400}
  />
  <Text fontSize={24}>{$data.name}</Text>
  <Text>{$data.description}</Text>
</Column>

User Avatar

// User list
@for (user in users) {
  <Row onClick={() => $navigate("/user/{user.id}")}>
    <Image
      src={user.avatar}
      transition:crossfade="avatar-{user.id}"
      w={48} h={48}
      cornerRadius={24}
    />
    <Text>{user.name}</Text>
  </Row>
}

// User profile
<Column>
  <Image
    src={$data.avatar}
    transition:crossfade="avatar-{$route.params.id}"
    w={120} h={120}
    cornerRadius={60}
  />
  <Text fontSize={32}>{$data.name}</Text>
</Column>

Multiple Shared Elements

Animate multiple elements on the same screen pair:

// Article list
<Card onClick={() => $navigate("/article/{article.id}")}>
  <Image
    src={article.cover}
    transition:crossfade="cover-{article.id}"
  />
  <Text
    transition:crossfade="title-{article.id}"
  >
    {article.title}
  </Text>
</Card>

// Article detail
<Column>
  <Image
    src={$data.cover}
    transition:crossfade="cover-{$route.params.id}"
    h={300}
  />
  <Text
    fontSize={28}
    transition:crossfade="title-{$route.params.id}"
  >
    {$data.title}
  </Text>
  <Text>{$data.body}</Text>
</Column>

With Navigation State

Pass preview data for instant rendering:

// List (pass preview data)
<Card onClick={() => $navigate(
  "/show/{show.id}",
  state = { poster: show.poster, title: show.name }
)}>
  <Image
    src={show.poster}
    transition:crossfade="poster-{show.id}"
  />
</Card>

// Detail (use preview until loaded)
suspend fun load(): ShowData {
  val preview = $route.state
  val show = $fetch.get(url = "...")
  return ShowData(show)
}

<Image
  src={$route.state?.poster ?: $data.posterUrl}
  transition:crossfade="poster-{$route.params.id}"
/>

Best Practices

  • Use descriptive, unique keys: "poster-{id}", not just "{id}"
  • Keep duration short (300-400ms) for snappy feel
  • Use type="element" for images, type="bounds" for containers
  • Combine with navigation state for smooth loading states
  • Limit to 1-3 shared elements per screen transition

Shared element transitions work with Compose's predictive back gesture. Users can scrub the animation by dragging the back gesture.

See Also