= Jetpack Compose Steppers Vertical & Horizontal - Coding Bihar

Best Premium Templates For App and Software Downloading Site. Made By HIVE-Store

Android Jetpack Compose

Stay ahead with the latest tools, trends, and best practices in Android Jetpack Compose App development

Jetpack Compose Steppers Vertical & Horizontal

Jetpack Compose Steppers Vertical & Horizontal - Coding Bihar
Jetpack Compose Steppers Vertical & Horizontal

Jetpack Compose Steppers: Create Beautiful Vertical & Horizontal Progress Indicators 

Jetpack Compose has completely changed the way we build Android UIs — declarative, fast, and customizable.

But when it comes to progress tracking or multi-step workflows (like onboarding or checkout screens), steppers can make your app look more polished and professional.

In this article, we’ll explore three reusable Jetpack Compose Stepper components: 

🧭 VerticalStepper timeline-style layout with optional images 
➡️ HorizontalStepper clean and modern linear layout 
CompactHorizontalStepper minimal, icon-only version for mobile UIs 

We’ll also look at how each part of the code works, why it’s written this way, and how you can customize it for your own projects. 

🎯 What is a Stepper? 

A Stepper visually represents progress through a sequence of steps. It’s often used in: 
Each step typically shows a title, an icon or number, and a connector line that indicates progress. 

🧩 Step 1: Defining the Data Model 

We start with a StepItem data class that describes each step. 
data class StepItem(
    val id: Int,
    val title: String,
    val subtitle: String? = null,
    val image: Painter? = null,
    val icon: ImageVector? = null,
    val completed: Boolean = false
)
Each step can have: 
  • title and optional subtitle 
  • either an image (like a user profile picture) or an icon 
  • completed flag to style finished steps differently 
This structure gives flexibility to support multiple visual styles without complex conditions. 

🧱 Step 2: Creating the Vertical Stepper 

The VerticalStepper displays steps stacked vertically — similar to a timeline. 

@Composable
fun VerticalStepper(
    steps: List,
    modifier: Modifier = Modifier,
    nodeSize: Dp = 56.dp,
    lineWidth: Dp = 2.dp,
    nodeContentPadding: Dp = 8.dp,
    accent: Color = MaterialTheme.colorScheme.primary,
    onStepClick: (StepItem) -> Unit = {}
) { ... }

🧠 How it works 

  • A Column arranges steps vertically. 
  • Each step has a circle node and a connector line (drawn using Canvas). 
  • If the previous step is completed, the line and node turn into the accent color (e.g., blue or green). 

  • Each node can display: 
  • an image (Painter) 
  • an icon (ImageVector) 
  • or fallback text (step number) 

Why use Canvas?

It gives pixel-level control to draw the connecting lines and lets you easily animate or customize colors later. 

✨ Example Preview 

The preview shows how steps render in a vertical timeline. 
VerticalStepper(
    steps = demoSteps,
    modifier = Modifier.padding(vertical = 8.dp)
)

➡️ Step 3: Building the Horizontal Stepper

Now let’s go horizontal! This version is perfect for forms, checkout processes, or tab-like navigation flows.
@Composable
fun HorizontalStepper(
    steps: List,
    modifier: Modifier = Modifier,
    nodeSize: Dp = 56.dp,
    lineHeight: Dp = 2.dp,
    accent: Color = MaterialTheme.colorScheme.primary,
    onStepClick: (StepItem) -> Unit = {}
) { ... }

🔍 What’s happening inside

  • Uses a Row layout to arrange steps side-by-side. 
  • Each step contains a circular node (with image/icon/text) and a small title below. 
  • Between each node, a connector line is drawn using a Box with height lineHeight. 
  • Completed steps show accent color; upcoming steps remain gray. 
You can easily extend this for animations — for example, animating the connector width when a step is marked complete. 

⚡ Step 4: CompactHorizontalStepper — Minimal and Mobile-Friendly

Sometimes you need a cleaner, space-saving stepper for compact UIs (like mobile or watch screens). That’s where CompactHorizontalStepper shines.
@Composable
fun CompactHorizontalStepper(
    steps: List,
    modifier: Modifier = Modifier,
    nodeSize: Dp = 40.dp,
    accent: Color = MaterialTheme.colorScheme.primary,
    onStepClick: (StepItem) -> Unit = {}
) { ... }
This version: 
  • Displays only icons or step numbers 
  • Keeps connectors slim and subtle
  • Uses smaller nodes and spacing 
  • Perfect for mobile step indicators (e.g., “Step 1 → Step 2 → Step 3”) 

🧠 Key Composable Concepts Used

Concept Purpose
Canvas Draws custom connectors between nodes
Surface Adds elevation & shape (for circular nodes)
Modifier.clickable Makes each node interactive
Box / Row / Column Layout containers
MaterialTheme.colorScheme Uses dynamic colors (light/dark mode support)

🖼️ Step 5: Preview and Demo

Jetpack Compose previews make it super easy to visualize your UI components instantly.
@Preview(showBackground = true)
@Composable
fun PreviewSteppers() {
    val demoSteps = listOf(
        StepItem(1, "Start", "Create account", completed = true),
        StepItem(2, "Verify", "Email verification", completed = true),
        StepItem(3, "Profile", "Add details"),
        StepItem(4, "Finish", "You're ready!")
    )

    MaterialTheme {
        Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
            Text(text = "VerticalStepper", fontWeight = FontWeight.Bold)
            VerticalStepper(steps = demoSteps)

            Divider(modifier = Modifier.padding(vertical = 8.dp))

            Text(text = "HorizontalStepper", fontWeight = FontWeight.Bold)
            HorizontalStepper(steps = demoSteps)

            Divider(modifier = Modifier.padding(vertical = 8.dp))

            Text(text = "CompactHorizontalStepper", fontWeight = FontWeight.Bold)
            CompactHorizontalStepper(steps = demoSteps)
        }
    }
}
You can instantly see all three stepper types in one preview!

🎨 Customization Tips

Here’s how you can tailor steppers to your brand or design system:
Property Description
accent Change progress color
nodeSize Adjust node circle size
lineWidth / lineHeight Change connector thickness
nodeContentPadding Control inner spacing for icons/images
tonalElevation Adjust 3D look of nodes

🔧 Integrating in Your App

You can easily plug these steppers into any Jetpack Compose screen:
VerticalStepper(
    steps = mySteps,
    accent = Color(0xFF2196F3),
    onStepClick = { step -> /* handle click */ }
)
To use network images, integrate Coil:
image = rememberAsyncImagePainter("https://example.com/avatar.png")

Final Compete Code:

package com.codingbihar.stepperdemo

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

// --- Models

data class StepItem(
    val id: Int,
    val title: String,
    val subtitle: String? = null,
    val image: Painter? = null,
    val icon: ImageVector? = null,
    val completed: Boolean = false
)

// --- VerticalStepper

@Composable
fun VerticalStepper(
    steps: List,
    modifier: Modifier = Modifier,
    nodeSize: Dp = 56.dp,
    lineWidth: Dp = 2.dp,
    nodeContentPadding: Dp = 8.dp,
    accent: Color = MaterialTheme.colorScheme.primary,
    onStepClick: (StepItem) -> Unit = {}
) {
    Column(modifier = modifier) {
        steps.forEachIndexed { index, step ->
            Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) {
                // left: connector + node
                Column(
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier
                        .width(nodeSize)
                        .padding(top = if (index == 0) 0.dp else 8.dp)
                ) {
                    // top connector
                    if (index != 0) {
                        Canvas(modifier = Modifier
                            .height(12.dp)
                            .width(lineWidth)
                        ) {
                            drawRect(color = if (steps[index - 1].completed) accent else Color.LightGray)
                        }
                    } else Spacer(modifier = Modifier.height(12.dp))

                    // node
                    Surface(
                        shape = CircleShape,
                        tonalElevation = 2.dp,
                        modifier = Modifier
                            .size(nodeSize)
                            .clickable { onStepClick(step) }
                    ) {
                        Box(contentAlignment = Alignment.Center) {
                            if (step.image != null) {
                                Image(
                                    painter = step.image,
                                    contentDescription = step.title,
                                    contentScale = ContentScale.Crop,
                                    modifier = Modifier
                                        .fillMaxSize()
                                )
                            } else if (step.icon != null) {
                                Icon(
                                    imageVector = step.icon,
                                    contentDescription = step.title,
                                    modifier = Modifier.size(nodeSize - nodeContentPadding)
                                )
                            } else {
                                // fallback: step number
                                Text(
                                    text = (index + 1).toString(),
                                    fontWeight = FontWeight.Bold,
                                    fontSize = 16.sp
                                )
                            }
                        }
                    }

                    // bottom connector
                    if (index != steps.lastIndex) {
                        Canvas(modifier = Modifier
                            .height(24.dp)
                            .width(lineWidth)
                        ) {
                            drawRect(color = if (step.completed) accent else Color.LightGray)
                        }
                    }
                }

                // right: content
                Column(modifier = Modifier
                    .padding(start = 12.dp, bottom = 12.dp)
                    .fillMaxWidth()
                ) {
                    Text(text = step.title, fontWeight = FontWeight.SemiBold)
                    step.subtitle?.let {
                        Text(text = it, style = MaterialTheme.typography.bodySmall)
                    }
                }
            }
        }
    }
}

// --- HorizontalStepper (full) -- simple clean horizontal layout

@Composable
fun HorizontalStepper(
    steps: List,
    modifier: Modifier = Modifier,
    nodeSize: Dp = 56.dp,
    lineHeight: Dp = 2.dp,
    accent: Color = MaterialTheme.colorScheme.primary,
    onStepClick: (StepItem) -> Unit = {}
) {
    Row(modifier = modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
        steps.forEachIndexed { index, step ->
            Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.clickable { onStepClick(step) }) {
                Surface(shape = CircleShape, modifier = Modifier.size(nodeSize)) {
                    Box(contentAlignment = Alignment.Center) {
                        if (step.image != null) {
                            Image(painter = step.image, contentDescription = step.title, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize())
                        } else if (step.icon != null) {
                            Icon(imageVector = step.icon, contentDescription = step.title, modifier = Modifier.size(nodeSize - 16.dp))
                        } else {
                            Text(text = (index + 1).toString(), fontWeight = FontWeight.Bold)
                        }
                    }
                }
                Spacer(modifier = Modifier.height(6.dp))
                Text(text = step.title, style = MaterialTheme.typography.bodySmall)
            }

            if (index != steps.lastIndex) {
                Spacer(modifier = Modifier.width(8.dp))
                // line
                Box(modifier = Modifier
                    .height(lineHeight)
                    .weight(1f)
                    .align(Alignment.CenterVertically)
                    .background(color = if (step.completed) accent else Color.LightGray)
                )
                Spacer(modifier = Modifier.width(8.dp))
            }
        }
    }
}

// --- CompactHorizontalStepper: icon-only compact version for mobile UIs

@Composable
fun CompactHorizontalStepper(
    steps: List,
    modifier: Modifier = Modifier,
    nodeSize: Dp = 40.dp,
    accent: Color = MaterialTheme.colorScheme.primary,
    onStepClick: (StepItem) -> Unit = {}
) {
    Row(modifier = modifier.padding(horizontal = 8.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically) {
        steps.forEachIndexed { index, step ->
            Box(modifier = Modifier
                .size(nodeSize)
                .clickable { onStepClick(step) }, contentAlignment = Alignment.Center) {
                Surface(shape = CircleShape, tonalElevation = if (step.completed) 4.dp else 0.dp) {
                    Box(contentAlignment = Alignment.Center, modifier = Modifier.size(nodeSize)) {
                        if (step.icon != null) {
                            Icon(imageVector = step.icon, contentDescription = step.title, modifier = Modifier.size(nodeSize - 12.dp))
                        } else {
                            Text(text = "${index + 1}", fontSize = 12.sp)
                        }
                    }
                }
            }

            if (index != steps.lastIndex) {
                Spacer(modifier = Modifier.width(8.dp))
                Box(modifier = Modifier
                    .height(2.dp)
                    .weight(1f)
                    .align(Alignment.CenterVertically)
                    .background(color = if (step.completed) accent else Color.LightGray)
                )
                Spacer(modifier = Modifier.width(8.dp))
            }
        }
    }
}

// --- Previews (demo data)

@Preview(showBackground = true)
@Composable
fun PreviewSteppers() {
    val demoSteps = remember {
        listOf(
            StepItem(1, "Start", "Create account", image = null, completed = true),
            StepItem(2, "Verify", "Email verification", image = null, completed = true),
            StepItem(3, "Profile", "Add details", image = null, completed = false),
            StepItem(4, "Finish", "You're ready!", image = null, completed = false)
        )
    }

    MaterialTheme {
        Column(modifier = Modifier.fillMaxSize().systemBarsPadding().padding(16.dp)) {
            Text(text = "VerticalStepper", fontWeight = FontWeight.Bold)
            VerticalStepper(steps = demoSteps, modifier = Modifier.padding(vertical = 8.dp))

            HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))

            Text(text = "HorizontalStepper", fontWeight = FontWeight.Bold)
            HorizontalStepper(steps = demoSteps, modifier = Modifier.fillMaxWidth())

            HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))

            Text(text = "CompactHorizontalStepper", fontWeight = FontWeight.Bold)
            CompactHorizontalStepper(steps = demoSteps)
        }
    }
}

Output:

Jetpack Compose Steppers Vertical & Horizontal Screenshot

💡 Final Thoughts

The beauty of Jetpack Compose lies in composability — writing small reusable building blocks that can scale. These Stepper components are a great example of how you can create clean, modern UI patterns with minimal effort.
✅ Works with Material 3 
✅ Supports dark mode 
✅ Fully customizable & reusable 
✅ Ideal for forms, onboarding, and progress tracking 

🔗 Wrap-Up 

We’ve built three polished steppers — vertical, horizontal, and compact — that can easily fit into any Compose project.

If you’re building modern Android apps, adding such thoughtful UI components not only improves user experience but also reflects design maturity. 
✨ Pro Tip: Try animating connectors or using icons from Material Icons to make the interaction feel alive.

Topics

Special Message

You Can Show Anything To Your Users