What You Will Build
A scrollable list of gradient cards that shift position and scale based on their scroll offset, creating a parallax depth effect. Cards closer to the center appear larger and more prominent, while cards near the edges shrink slightly. This effect is used in app store featured sections, travel apps, and media galleries.
Why This Pattern Matters
Parallax scrolling creates a sense of depth that flat lists lack. By using graphicsLayer with scroll position data, each card transforms independently based on where it sits in the viewport, all without any external animation library.
Step 1: Define the Card Model
data class ParallaxItem(
val id: Int,
val title: String,
val subtitle: String,
val colors: List<Color>
)
Step 2: Build the Parallax List
@Composable
fun ParallaxCardsScreen() {
val items = remember {
listOf(
ParallaxItem(0, "Mountain", "Explore the peaks",
listOf(Color(0xFF1E3A5F), Color(0xFF4A90D9))),
ParallaxItem(1, "Ocean", "Dive into the deep",
listOf(Color(0xFF0E4D64), Color(0xFF00B4DB))),
ParallaxItem(2, "Forest", "Walk among giants",
listOf(Color(0xFF1B4332), Color(0xFF52B788))),
ParallaxItem(3, "Desert", "Endless horizons",
listOf(Color(0xFF78350F), Color(0xFFFBBF24)))
)
}
val listState = rememberLazyListState()
LazyColumn(
state = listState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(items, key = { it.id }) { item ->
var offsetY by remember { mutableFloatStateOf(0f) }
Surface(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.onGloballyPositioned { coords ->
offsetY = coords.positionInParent().y
}
.graphicsLayer {
val parallaxOffset = (offsetY - 400f) * 0.1f
translationY = parallaxOffset
val scale = (1f - abs(offsetY - 400f) / 2000f)
.coerceIn(0.9f, 1f)
scaleX = scale
scaleY = scale
},
shape = RoundedCornerShape(20.dp),
shadowElevation = 8.dp
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.horizontalGradient(item.colors)
),
contentAlignment = Alignment.BottomStart
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(item.title, fontSize = 28.sp,
fontWeight = FontWeight.Bold, color = Color.White)
Text(item.subtitle, fontSize = 16.sp,
color = Color.White.copy(alpha = 0.8f))
}
}
}
}
}
}
Tips and Pitfalls
- graphicsLayer is GPU-accelerated and does not trigger recomposition, making it ideal for scroll-driven transforms.
- The 0.1f parallax factor creates subtle motion. Values above 0.3 can look disorienting.
- Coerce scale between 0.9f and 1.0f so cards never shrink too much or grow larger than normal.
- onGloballyPositioned fires on every scroll frame, so keep the lambda lightweight.