UI: Step Navigator and Controls

Sep 23, 2026

I decided to build a reusable Step Navigator component that shows the user's progress through the form when create a screening.

Requirements

Before implementing the component, I defined a few requirements:

  • Support active, complete, and normal states
  • Display a step title and an optional description
  • Support both horizontal and vertical orientations
  • Allow users to click a step to navigate between steps
  • Keep the component reusable by allowing the parent component to control the steps and current state

The last requirement was particularly important. The Step Navigator should be responsible for displaying the steps, not for managing the form itself. The parent component should control which step is active and what happens when a user selects another step.

For example:

<StepNavigator
  steps="{steps}"
  currentStep="{currentStep}"
  onStepClick="{setCurrentStep}"
  orientation="horizontal"
/>

This keeps the navigator independent from the screening form and allows the same component to be reused in other multi-step workflows.

Structure of the Step Navigator

I separated the horizontal and vertical implementations into two components because the layout and connector positioning are fundamentally different.

The main Step Navigator determines which implementation to render based on the orientation:

StepNavigator
	├── HorizontalStepNavigator
	└── VerticalStepNavigator

The navigator uses an ordered list (<ol>), with each step represented by a list item (<li>).

Conceptually, each step contains:

Step
├── Indicator
├── Label
├── Description
└── Connector

The indicator, label, and description make up the visible content of the step, while the connector visually links it to the next step.

Styling

Indicator

The indicator is the round icon for each steps. It should show the current step number and the status of each step.

  • completed: Background filled and check icon
  • active: background filled and outline the indicator
  • normal: regular border and number

Step Navigator UI The completed and active steps use the primary colour, while upcoming steps remain visually muted.

Connector

The connector provides another visual indication of progress. Both completed and active steps fill the connector leading toward the next step. This makes it easier to understand how far the user has progressed through the workflow.

Connector alignment Problem

The most challenging part of building the Step Navigator was not the indicator itself, but the connector.

My initial idea is to place a <hr> after each step:

(Indicator + Label + Description) -------
(Indicator + Label + Description) -------
(Indicator + Label + Description)

This seemed straightforwards, but it introduced several layout problems.

Problem 1: Content affects connector length

Each step can have an optional description, and descriptions can have different lengths. For example:

Movie
Select a movie

Details
Choose when the screening
will take place

Review
Confirm your screening

If the connector is part of the same flex layout as the step content, the size of the content affects the available space for the connector.

A longer description can make the step larger, which can cause the connector to behave differently from the other steps.

Problem 2: Content affected alignment

Even if I use flexbox to center the content, different description lengths can affect the height of each list item. The indicators may be centered correctly within their individual elements, but the connector is no longer visually consistent.

Equal Width Steps

The first part of the solution was to make every step take up an equal amount of horizontal space. Each <li> uses flex-1:

<li className="relative flex-1"></li>

With four steps, each step receives approximately 25% of the available width.

|--------|--------|--------|--------|
   25%       25%      25%      25%

This ensures that the indicators remain evenly distributed regardless of the length of their labels or descriptions.

However, this alone does not solve the connector problem.

Positioning the Connector

The connector should not depend on the width of the text content. Instead, it should be positioned relative to the center of the step.

Since each step has an equal width, the connector can start from the center of the current step and end at the center of the next step.

I also wanted some visual space between the connector and the circular indicators rather than having the line touch the circles.

The idea is:

        1.5rem gap
          
           ─────────     
                   
          start    end

The connector can therefore be absolutely positioned inside the list item.

Its starting point is based on the center of the current list item, with a gap after the indicator. Its ending point is positioned before the center of the next list item, leaving the same gap on the other side.

Conceptually:

current step                       next step
                                      
                                      
     │← gap →│───────────────│← gap →│
             connector

Because the list items have equal widths, the connector remains evenly spaced even when the labels and descriptions have different lengths.

Why I Separated Horizontal and Vertical Layouts

Although both orientations represent the same concept, their connector geometry is different.

For horizontal orientation:

 ─────  ─────  ───── 

The connector needs to be positioned horizontally between equally spaced indicators.

For vertical orientation:








The connector can instead live in the indicator column and extend vertically between steps.

This made it cleaner to have two components rather than trying to build one component with a large number of conditional styles.

The parent API remains the same:

<StepNavigator
  steps="{steps}"
  currentStep="{currentStep}"
  orientation="vertical"
/>

but the internal layout is handled by the appropriate implementation.

Keeping the Navigator Reusable

One of the design decisions I wanted to preserve was keeping the Step Navigator unaware of the actual form.

The screening form owns the state:

Screening Form
      
      ├── currentStep
      ├── steps
      └── navigation logic
              
              
       Step Navigator

The navigator receives the information it needs to render:

type Step = {
  id: string;
  label: string;
  description?: string;
};

The parent decides what the steps mean.

For CineJourney, the steps are:

Movie  Details  Discussion  Review

But another workflow could use:

Profile  Preferences  Confirmation

without requiring any changes to the navigator itself.

What I Learned

The biggest lesson from building this component was that content layout and progress-indicator layout should not be tightly coupled.

My first instinct was to put the indicator, content, and connector into the same flex layout. That works for a simple stepper, but once descriptions have variable lengths, the content starts influencing the geometry of the progress indicator.

The solution was to treat the navigator as two separate concerns:

Content
Indicator + Label + Description

Progress
Indicator + Connector

The indicators need predictable positioning, while the text content should be allowed to grow naturally.

This is also why the horizontal and vertical implementations ended up having different internal structures. They share the same component API and state model, but their visual geometry is different enough that forcing them into a single layout would make the component harder to maintain.

For me, the interesting part of building a seemingly simple UI component was not writing the JSX. It was identifying which pieces of the layout should influence each other and which should remain independent.