Sitemap

Building Sliding Stepper Component in React — Use case of useRef

6 min readJul 26, 2024

--

Sliding Stepper Component in Action
Sliding Stepper Components

Have you ever wondered what is the use of useRef hooks in React (read here for more)? One of its uses is to access the real DOM of HTML elements. In this case, it can be used to build a dynamic component such as a sliding stepper.

Press enter or click to view image in full size
useRef in React

Now you might be wondering, how is useRef used in this situation? For starter without useRef (and the translating part), the component will look like this,

Sliding Stepper Components without useRef and Translation

“Why don’t we just translate the stepper by x-px?” If the steps have a fixed width for all of the steps in the stepper, then it might work, but what if it looks like this,

Press enter or click to view image in full size

and that is where the issue is. We need the specific width of each of the steps, to get that information we need to access the element DOM, in this case, we can use useRef for it. By using useRef we can dynamically get the width of each of the steps like below,

Firstly, we need to create a component for the stepper itself, Let’s call it “Stepper”, the component will accept steps which is an array of objects containing the label of the step and the current step which is a type of number as its props,

import Step from "./Step";
import styles from "./Stepper.module.css";
import { useStepperImpl } from "./useStepperImpl";
export type TStep = {
label: string;
};
const Stepper = ({
steps,
currentStep,
}: {
steps: TStep[];
currentStep: number;
}) => {
return (
<div
className={styles.stepperContainer}
>
{steps.map((step, index) => (
<div>
<Step
label={step.label}
index={index}
isActive={currentStep === index}
/>
</div>
))}
</div>
);
};
export default Stepper;

Inside that “Stepper” component, we also render “Step” components, where we map through the steps array to render it. Now, “How do we get the width of the step?”, here is where useRef comes in, when we render each of the steps, we will assign as ref to it, where all of the refs are kept inside of stepIndexRef,

import Step from "./Step";
import styles from "./Stepper.module.css";
import { useStepperImpl } from "./useStepperImpl";
export type TStep = {
label: string;
};
const Stepper = ({
steps,
currentStep,
}: {
steps: TStep[];
currentStep: number;
}) => {
const stepsIndexRefs = useRef({});
return (
<div
className={styles.stepperContainer}
>
{steps.map((step, index) => (
<div
ref={(el) => ((stepsIndexRefs as any).current[index] = el)} <-- HERE
>
<Step
label={step.label}
index={index}
isActive={currentStep === index}
/>
</div>
))}
</div>
);
};
export default Stepper;

After assigning ref to each “ Step “ component, what are the next steps? Now we have to do some primary school-level math,

Let’s see the illustration below,

Press enter or click to view image in full size

From the illustration above, it’s clear that we need to calculate a to translate the whole “Stepper” component by (-a) px in the x-axis, to get that value, we can sum all of the steps width until the currentStep, that process can be done with the code below,

Object.entries(stepsIndexRefs.current)
.splice(0, currentStep)
.map((val) => {
return (
val[1] as HTMLDivElemet
)?.getBoundingClientRect().width + 16 ?? 0
.reduce((prev, current) => prev + curr, 0)

You might be wondering what the value “16” is doing there. That value is the gap between each of the steps. After having the value a we can continue translating the stepper function in the x-axis by -a px with the following code,

Press enter or click to view image in full size
Press enter or click to view image in full size
Translating by -a px

By doing that, we have a somewhat working sliding stepper component,

Get Felix Fernando’s stories in your inbox

Join Medium for free to get updates from this writer.

But currently, it’s too clunky looking, to smooth it out we can just add a CSS transition property to the parent of the Stepper component in this case stepperContainer. With that small addition, we already have a smooth-moving Sliding Stepper Component,

Press enter or click to view image in full size
Press enter or click to view image in full size
Adding CSS Transitions

With the current result, does it bother you that when the Stepper Component is reaching the end, there is a lot of whitespace which makes it feel not that polished? To fix that, we need to do another math for it,

Press enter or click to view image in full size

To fix this, we need to bound the translation value. Based on the illustration above, we need to bound it by the value -d px. To do that, we must have the value of d + e or in this case, the total width of the stepper, and c is the stepper component container width.

Before calculating the total width of the stepper, we need the container size, for this, we can just attach a ref to the parent that wraps the stepper container as follows, with containerRef a value of useRef

<div
className="w-1/2 p-4 overflow-hidden border-2 border-orange-700 rounded-sm"
ref={containerRef}
>
<Stepper
steps={steps}
currentStep={currentStep}
containerRef={containerRef}
/>
</div>

To calculate the total width of the stepper, we can just iterate through the stepIndexRefs and sum all of the width values for it,

const totalStepperWidth = Object.entries(stepsIndexRefs.current)
.map((val) => {
return ((val[1] as HTMLDivElement)?.getBoundingClientRect().width ??
0) as number;
})
.reduce((prev, curr) => prev + curr, 0);
const containerWidth = containerRef.current?.getBoundingClientRect().width;

with both of the values, the translate variable can be modified as follows to accommodate our needs, which is to bound the translate value,

const translate =
(containerWidth ?? 0) > totalStepperWidth
? 0
: Math.min(
totalStepperWidth -
(containerWidth ?? 0) +
(totalSteps + 1) * 16,
Object.entries(stepsIndexRefs.current)
.splice(0, currentStep)
.map((val) => {
return (
(
val[1] as HTMLDivElement
)?.getBoundingClientRect().width + 16 ?? 0
);
})
.reduce((prev, curr) => prev + curr, 0)

The condition (containerWidth ?? 0) > totalStepperWidth itself is to stop the translation if all of the steps fit in the stepper container.

By doing all ofthat, we will get a pleasing and smooth-moving Sliding Stepper Component like the following,

Final Result

For a cleaner code, I usually separate the logic into a custom hook and the component itself, by doing that, the end result of the Sliding Stepper Component are as follows,

/* eslint-disable @typescript-eslint/no-explicit-any */
import Step from "./Step";
import styles from "./Stepper.module.css";
import { useStepperImpl } from "./useStepperImpl";

export type TStep = {
label: string;
};

const Stepper = ({
steps,
currentStep,
containerRef,
}: {
steps: TStep[];
currentStep: number;
containerRef: React.RefObject<HTMLDivElement>;
}) => {
const { stepsIndexRefs, translate } = useStepperImpl({
containerRef,
currentStep,
totalSteps: steps.length,
});

return (
<div
className={styles.stepperContainer}
style={{
transform: `translateX(-${translate}px)`,
}}
>
{steps.map((step, index) => (
<div
ref={(el) => ((stepsIndexRefs as any).current[index] = el)}
>
<Step
label={step.label}
index={index}
isActive={currentStep === index}
/>
</div>
))}
</div>
);
};

export default Stepper;

and that’s how you implement a Sliding Stepper Component by using useRef hook in React.

For a full implementation of this component, you can check out the Github repository below,

Check me out on my other socials below!

My Socials:

Github: https://github.com/FelixFern
Personal Website: https://felixfern.me/
LinkedIn: https://www.linkedin.com/in/felix-fern/

--

--

Felix Fernando
Felix Fernando

Written by Felix Fernando

a Frontend Engineer that's trying to write technical stuff.