Type something to search...
Building a Customizable Image Slider in React Using Hooks, SCSS, and TypeScript

Building a Customizable Image Slider in React Using Hooks, SCSS, and TypeScript

Introduction

In this tutorial, we will be building a customizable image slider in React using hooks, SCSS, and TypeScript. An image slider is a common UI element used in web applications to display a set of images that can be scrolled or navigated through. With React, building an image slider becomes easier and more modular.

Hooks are a new addition to React 16.8 that allows you to use state and other React features without writing a class. Hooks enable the creation of reusable and composable logic for React components.

SCSS is a preprocessor for CSS, which makes it easier to write and maintain large stylesheets. It allows you to use variables, mixins, and functions to create more modular and maintainable styles.

TypeScript is a typed superset of JavaScript that adds type annotations and other features to make it easier to catch errors and refactor code. TypeScript improves the developer experience and makes code easier to understand and maintain.

By the end of this tutorial, you will have a basic understanding of how to create a customizable image slider in React using hooks, SCSS, and TypeScript. You will also learn how to create a reusable component that can be easily customized and styled to fit your needs.

Prerequisites

Before starting to build the customizable image slider in React, you should have a basic understanding of React, JavaScript, and CSS. You should be familiar with React hooks, including useState and useEffect, and have a working knowledge of TypeScript. Additionally, you should have Node.js and npm (Node Package Manager) installed on your machine, as we will be using them to set up the project and manage its dependencies.

If you are new to React or TypeScript, it is recommended that you first complete some beginner-level tutorials to familiarize yourself with the fundamentals of the technologies. Once you have a solid understanding of the basics, you will be better equipped to follow along with this tutorial and build your own customizable image slider.

Getting Started

n this section, we will guide you through setting up the project. To start with, we will create a new React project using vite. Vite is a fast and efficient build tool that enables you to develop your React application quickly. It is built on top of Rollup and ESBuild, which makes it a lightweight and user-friendly tool.

To create a new project, open up your terminal and run the following command:

Terminal window
npx create-vite react-hooks-slider --template react-ts

This command will create a new React project with the name react-hooks-slider using the react-ts template, which includes TypeScript support. Once the project is created, navigate into the project directory using the cd command:

Terminal window
cd react-hooks-slider

This will create a new React project using TypeScript. After that, we will install the dependencie for SCSS. To do this, run the following command:

Terminal window
npm install -D sass

Now that we have created our project, we can begin setting up our slider component using React, Hooks, SCSS, and TypeScript. In the next section, we will start by creating a new React component for our slider.

Creating the Slider Component

The first step in building our customizable image slider is to create a React component that will render our slider. We’ll call this component Slider.

To create the Slider component, we’ll first need to import React and the necessary hooks from React, including useState, useEffect, and useRef. We’ll also need to import any external libraries or components that we’ll be using in our slider.

Next, we’ll define our Slider function component and set up its initial state using the useState hook. We’ll keep track of the current index of the active slide and the previous and next slide indexes. We’ll also create a ref using the useRef hook that will allow us to access the container element of our slider.

Then, we’ll set up the useEffect hook to update the slider’s state and animate the slides whenever the current index changes. This hook will listen for changes to the current index and adjust the previous and next slide indexes accordingly. It will also update the position of the slider using CSS transforms to animate the slides.

Creating the Component Structure

In this section, we will create the basic structure of our slider component. We will start by creating a new folder called components inside the src folder. Inside the components folder, we will create a new directory called Slider and Icons. The Slider directory will contain the files for our slider component, and the Icons directory will contain the SVG as React components.

At the end of this section, our project structure will look like this:

Terminal window
react-hooks-slider
├── dist
├── node_modules
├── public
├── src
├── components
├── Icons
├── RightArrowIcon.test.tsx
└── RightArrowIcon.tsx
└── Slider
├── index.tsx
├── Slide.test.tsx
├── Slide.tsx
├── Slider.test.tsx
├── Slider.tsx
├── SliderControl.test.tsx
├── SliderControl.tsx
└── style.scss
├── data
└── slider.data.json
├── App.tsx
├── main.tsx
├── style.scss
└── vite-env.d.ts
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

Now that we have created our project structure, we can start creating our slider component. To do this, at first, we will create a new file called RightArrowIcon.tsx inside the Icons directory. This file will contain the SVG for the right arrow icon that we will use in our slider component.

src/components/Icons/RightArrowIcon.tsx
1
import React from 'react';
2
3
// type
4
export type Props = {
5
fill?: string;
6
size?: string;
7
[x: string]: any;
8
};
9
10
const index = (props: Props) => {
11
const {fill = 'currentColor', size = '24', ...otherProps} = props;
12
13
return (
14
<svg
15
xmlns="http://www.w3.org/2000/svg"
16
width={size}
17
height={size}
18
viewBox="0 0 24 24"
19
fill={fill}
20
{...otherProps}
21
>
22
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z"></path>
23
</svg>
24
);
25
};
26
27
export default index;

In the above code, we have created a new React component called RightArrowIcon. This component will render the SVG for the right arrow icon. We have also added the fill and size props to the component, which will allow us to customize the color and size of the icon.

Next, we will create a new file called Slide.tsx inside the Slider directory. This file will contain the main slider component. We will start by importing the RightArrowIcon component that we created in the previous step.

src/components/Slider/Slide.tsx
1
import React, {useRef, useEffect, MouseEvent} from 'react';
2
3
// icons
4
import RightArrowIcon from '../Icons/RightArrowIcon';
5
6
// types
7
export type Tag = {
8
name: string;
9
};
10
11
export type Link = {
12
name: string;
13
url: string;
14
};
15
16
export type SlideData = {
17
index: number;
18
src: string;
19
headline: string;
20
direction?: string;
21
tags?: Tag[];
22
links?: Link[];
23
};
24
25
export type SlideProps = {
26
slide: SlideData;
27
index: number;
28
current: number;
29
handleSlideClick: (e: MouseEvent<HTMLDivElement>) => void;
30
};
31
32
const Slide = ({slide, index, current, handleSlideClick}: SlideProps) => {
33
const slideRef = useRef<HTMLDivElement>(null);
34
35
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
36
const el = slideRef.current;
37
const r = el!.getBoundingClientRect();
38
39
el!.style.setProperty(
40
'--x',
41
(e.clientX - (r.left + Math.floor(r.width / 2))).toString(),
42
);
43
el!.style.setProperty(
44
'--y',
45
(e.clientY - (r.top + Math.floor(r.height / 2))).toString(),
46
);
47
};
48
49
const handleMouseLeave = (e: MouseEvent<HTMLDivElement>) => {
50
const el = slideRef.current;
51
if (el) {
52
el.style.setProperty('--x', '0');
53
el.style.setProperty('--y', '0');
54
}
55
};
56
57
useEffect(() => {
58
const el = slideRef.current!.querySelector('img');
59
el!.style.opacity = '1';
60
}, []);
61
62
const {src, headline, direction, tags, links} = slide;
63
let classNames = 'slide';
64
65
if (current === index) classNames += ' slide--current';
66
else if (current - 1 === index) classNames += ' slide--previous';
67
else if (current + 1 === index) classNames += ' slide--next';
68
69
return (
70
<div
71
ref={slideRef}
72
className={classNames}
73
onClick={handleSlideClick}
74
onMouseMove={handleMouseMove}
75
onMouseLeave={handleMouseLeave}
76
data-index={index}
77
>
78
<div className={'slide__image-wrapper'}>
79
<img className={'slide__image'} alt={headline} src={src} />
80
</div>
81
82
<div className={'slide__overlay'}>
83
<div className={'slide__content'}>
84
<h2 className={'slide__content--headline'}>{headline}</h2>
85
86
{direction && <p className={''}>{direction}</p>}
87
88
{tags && (
89
<div className={'slide__content--tag-wrapper'}>
90
Tags:{' '}
91
{tags.map((tag: Tag, index: number) => (
92
<span key={index} className={'slide__content--tag'}>
93
{tag?.name}
94
</span>
95
))}
96
</div>
97
)}
98
99
<div className={'slide__content--button-wrapper'}>
100
{links?.map((link: Link, index: number) => (
101
<a
102
key={index}
103
className={'slide__content--button'}
104
href={link?.url}
105
target={'__blank'}
106
>
107
{link?.name}{' '}
108
<span className={'slide__content--button__icon'}>
109
<RightArrowIcon />
110
</span>
111
</a>
112
))}
113
</div>
114
</div>
115
</div>
116
</div>
117
);
118
};
119
120
export default Slide;

In the above code, we have created a new React component called Slide. This component will render the main slider component. We have also added the slide, index, current, and handleSlideClick props to the component, which will allow us to pass the data for the slider and handle the click event on the slider.

Next, we will create a new file called SliderControl.tsx inside the Slider directory. This file will contain the main slider component. We will start by importing the Slide component that we created in the previous step.

src/components/Slider/SliderControl.tsx
1
import React, {MouseEvent} from 'react';
2
3
// types
4
export type SliderControlProps = {
5
type: string;
6
title: string;
7
handleClick: (e: MouseEvent<HTMLButtonElement>) => void;
8
};
9
10
const SliderControl = ({type, title, handleClick}: SliderControlProps) => {
11
return (
12
<button className={`btn btn--${type}`} title={title} onClick={handleClick}>
13
<svg className={'icon'} viewBox={'0 0 24 24'}>
14
<path d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z" />
15
</svg>
16
</button>
17
);
18
};
19
20
export default SliderControl;

In the above code, we have created a new React component called SliderControl. This component will render the slider control buttons. We have also added the type, title, and handleClick props to the component, which will allow us to pass the data for the slider control buttons and handle the click event on the slider control buttons.

Next, we will create a new file called Slider.tsx inside the Slider directory. This file will contain the main slider component. We will start by importing the Slide and SliderControl components that we created in the previous steps.

src/components/Slider/Slider.tsx
1
import React, {useState, MouseEvent} from 'react';
2
import Slide, {SlideData} from './Slide';
3
import SliderControl from './SliderControl';
4
5
export type SliderProps = {
6
slides: SlideData[];
7
heading: string;
8
};
9
10
const Slider = ({slides, heading}: SliderProps) => {
11
const [current, setCurrent] = useState(0);
12
const headingId = `slider-heading__${heading
13
.replace(/\s+/g, '-')
14
.toLowerCase()}`;
15
const wrapperTransform = {
16
transform: `translateX(-${current * (100 / slides.length)}%)`,
17
};
18
19
const handlePreviousClick = () => {
20
const previous = current - 1;
21
22
setCurrent(previous < 0 ? slides.length - 1 : previous);
23
};
24
25
const handleNextClick = () => {
26
const next = current + 1;
27
28
setCurrent(next === slides.length ? 0 : next);
29
};
30
31
const handleSlideClick = (e: MouseEvent<HTMLDivElement>) => {
32
const index = e.currentTarget?.getAttribute('data-index');
33
if (index && current !== +index) {
34
setCurrent(+index);
35
}
36
};
37
38
return (
39
<>
40
<div className={'slider'} aria-labelledby={headingId}>
41
<div className={'slider__wrapper'} style={wrapperTransform}>
42
<h3 id={headingId} className={'visuallyhidden'}>
43
{heading}
44
</h3>
45
46
{slides.map((slide, index: number) => (
47
<Slide
48
key={index}
49
index={index}
50
slide={slide}
51
current={current}
52
handleSlideClick={handleSlideClick}
53
/>
54
))}
55
</div>
56
</div>
57
58
<div className={'slider__controls'}>
59
<SliderControl
60
type={'previous'}
61
title={'Go to previous slide'}
62
handleClick={handlePreviousClick}
63
/>
64
65
<SliderControl
66
type={'next'}
67
title={'Go to next slide'}
68
handleClick={handleNextClick}
69
/>
70
</div>
71
</>
72
);
73
};
74
75
export default Slider;

In the above code, we have created a new React component called Slider. This component will render the main slider component. We have also added the slides and heading props to the component, which will allow us to pass the data for the slider.

Next, we will create a new file called index.tsx inside the Slider directory. This file will contain the main slider component. We will start by importing the Slider component that we created in the previous step.

src/components/Slider/index.tsx
1
import React from 'react';
2
import Slider from './Slider';
3
import {SlideData} from './Slide';
4
import './style.scss';
5
6
const SliderComponent = ({
7
slides,
8
heading,
9
}: {
10
slides: SlideData[];
11
heading: string;
12
}) => {
13
return (
14
<div>
15
<Slider slides={slides} heading={heading} />
16
</div>
17
);
18
};
19
20
export default SliderComponent;

In the above code, we have created a new React component called SliderComponent. This component will render the main slider component. We have also added the slides and heading props to the component, which will allow us to pass the data for the slider.

Next, we will create a new file called style.scss inside the Slider directory. This file will contain the main slider component styles.

src/components/Slider/style.scss
1
:root {
2
--color-primary: #6b7a8f;
3
--color-secondary: #101118;
4
--color-accent: #1d1f2f;
5
--color-focus: #6d64f7;
6
--base-duration: 600ms;
7
--base-ease: cubic-bezier(0.25, 0.46, 0.45, 0.84);
8
}
9
10
.visuallyhidden {
11
clip: rect(0.0625rem, 0.0625rem, 0.0625rem, 0.0625rem);
12
height: 0.0625rem;
13
overflow: hidden;
14
position: absolute !important;
15
white-space: nowrap;
16
width: 0.0625rem;
17
}
18
19
.icon {
20
fill: var(--color-primary);
21
width: 100%;
22
}
23
24
.btn {
25
background-color: var(--color-primary);
26
border: none;
27
border-radius: 0.125rem;
28
color: white;
29
cursor: pointer;
30
font-family: inherit;
31
font-size: inherit;
32
padding: 1rem 2.5rem 1.125rem;
33
34
&:focus {
35
outline-color: var(--color-focus);
36
outline-offset: 0.125rem;
37
outline-style: solid;
38
outline-width: 0.1875rem;
39
}
40
41
&:active {
42
transform: translateY(0.0625rem);
43
}
44
}
45
46
.slider {
47
--slide-size: 55vmin;
48
--slide-margin: 4vmin;
49
height: var(--slide-size);
50
width: var(--slide-size);
51
margin: 0 auto;
52
position: relative;
53
54
&__wrapper {
55
display: flex;
56
margin: 0 calc(var(--slide-margin) * -1);
57
position: absolute;
58
transition: transform var(--base-duration) cubic-bezier(0.25, 1, 0.35, 1);
59
}
60
61
&__controls {
62
display: flex;
63
justify-content: center;
64
width: 100%;
65
margin-top: 0.5rem;
66
67
.btn {
68
--size: 3rem;
69
align-items: center;
70
background-color: transparent;
71
border: 0.188rem solid transparent;
72
border-radius: 100%;
73
display: flex;
74
height: var(--size);
75
width: var(--size);
76
padding: 0;
77
78
&:focus {
79
border-color: var(--color-focus);
80
outline: none;
81
}
82
83
&--previous {
84
> * {
85
transform: rotate(180deg);
86
}
87
}
88
}
89
}
90
}
91
92
.slide {
93
align-items: center;
94
color: white;
95
display: flex;
96
flex: 1;
97
flex-direction: column;
98
height: var(--slide-size);
99
justify-content: center;
100
margin: 0 var(--slide-margin);
101
opacity: 0.25;
102
position: relative;
103
text-align: center;
104
transition:
105
opacity calc(var(--base-duration) / 2) var(--base-ease),
106
transform calc(var(--base-duration) / 2) var(--base-ease);
107
width: var(--slide-size);
108
z-index: 1;
109
110
&__image {
111
--d: 20;
112
height: 110%;
113
-o-object-fit: cover;
114
object-fit: cover;
115
opacity: 0;
116
pointer-events: none;
117
transition:
118
opacity var(--base-duration) var(--base-ease),
119
transform var(--base-duration) var(--base-ease);
120
-webkit-user-select: none;
121
-moz-user-select: none;
122
-ms-user-select: none;
123
user-select: none;
124
width: 110%;
125
126
&-wrapper {
127
background-color: var(--color-accent);
128
border-radius: 1%;
129
height: 100%;
130
left: 0%;
131
overflow: hidden;
132
position: absolute;
133
top: 0%;
134
transition: transform calc(var(--base-duration) / 4) var(--base-ease);
135
width: 100%;
136
}
137
}
138
139
&__overlay {
140
position: absolute;
141
top: 0;
142
bottom: 0;
143
left: 0;
144
right: 0;
145
height: 100%;
146
width: 100%;
147
transition: 0.5s ease;
148
background-color: #00000080;
149
transition: transform calc(var(--base-duration) / 4) var(--base-ease);
150
}
151
152
&__content {
153
--d: 60;
154
display: flex;
155
flex-direction: column;
156
align-items: center;
157
gap: 1rem;
158
opacity: 0;
159
padding: 1rem;
160
position: relative;
161
transition: transform var(--base-duration) var(--base-ease);
162
visibility: hidden;
163
164
&--headline {
165
font-size: 3rem;
166
font-weight: 600;
167
position: relative;
168
color: white;
169
}
170
171
&--tag {
172
display: flex;
173
justify-content: center;
174
align-items: center;
175
gap: 0.3rem;
176
177
&-wrapper {
178
display: flex;
179
flex-wrap: wrap;
180
align-items: center;
181
gap: 0.5rem;
182
margin-top: 0.5rem;
183
user-select: none;
184
}
185
}
186
187
&--button {
188
width: max-content;
189
color: var(--first-color);
190
font-size: var(--small-font-size);
191
display: flex;
192
align-items: center;
193
column-gap: 0.25rem;
194
195
&-wrapper {
196
display: flex;
197
flex-wrap: wrap;
198
align-items: center;
199
gap: 0.5rem;
200
margin-top: 0.5rem;
201
user-select: none;
202
}
203
204
&__icon {
205
width: 1rem;
206
height: 1rem;
207
transition: 0.4s;
208
209
svg {
210
width: 1rem;
211
height: 1rem;
212
}
213
}
214
215
&:hover &__icon {
216
transform: translateX(0.25rem);
217
}
218
}
219
}
220
221
&--previous {
222
&:hover {
223
opacity: 0.5;
224
transform: translateX(2%);
225
}
226
cursor: w-resize;
227
}
228
229
&--current {
230
--x: 0;
231
--y: 0;
232
--d: 50;
233
opacity: 1;
234
pointer-events: auto;
235
-webkit-user-select: auto;
236
-moz-user-select: auto;
237
-ms-user-select: auto;
238
user-select: auto;
239
240
.slide {
241
&__content {
242
-webkit-animation: fade-in calc(var(--base-duration) / 2)
243
var(--base-ease) forwards;
244
animation: fade-in calc(var(--base-duration) / 2) var(--base-ease)
245
forwards;
246
visibility: visible;
247
}
248
}
249
}
250
251
&--next {
252
&:hover {
253
opacity: 0.5;
254
transform: translateX(-2%);
255
}
256
cursor: e-resize;
257
}
258
}
259
260
@media screen and (max-width: 568px) {
261
.slider {
262
--slide-size: 90vmin;
263
}
264
}
265
266
@media (hover: hover) {
267
.slide {
268
&--current {
269
&:hover {
270
.slide {
271
&__image {
272
&-wrapper {
273
transform: scale(1.025)
274
translate(
275
calc(var(--x) / var(--d) * 0.063rem),
276
calc(var(--y) / var(--d) * 0.063rem)
277
);
278
}
279
}
280
281
&__overlay {
282
transform: scale(1.025)
283
translate(
284
calc(var(--x) / var(--d) * 0.063rem),
285
calc(var(--y) / var(--d) * 0.063rem)
286
);
287
}
288
}
289
}
290
.slide {
291
&__image {
292
transform: translate(
293
calc(var(--x) / var(--d) * 0.063rem),
294
calc(var(--y) / var(--d) * 0.063rem)
295
);
296
}
297
298
&__content {
299
transform: translate(
300
calc(var(--x) / var(--d) * -0.063rem),
301
calc(var(--y) / var(--d) * -0.063rem)
302
);
303
}
304
}
305
}
306
}
307
.slide {
308
&__overlay {
309
transform: translate(
310
calc(var(--x) / var(--d) * 0.063rem),
311
calc(var(--y) / var(--d) * 0.063rem)
312
);
313
}
314
}
315
}
316
317
@-webkit-keyframes fade-in {
318
from {
319
opacity: 0;
320
}
321
to {
322
opacity: 1;
323
}
324
}
325
326
@keyframes fade-in {
327
from {
328
opacity: 0;
329
}
330
to {
331
opacity: 1;
332
}
333
}

Running the Slider Component

Now that we have the slider component, we need to add it at App.tsx:

App.tsx
1
import React from 'react';
2
import './style.scss';
3
4
// Import the slider component
5
import Slider from './components/Slider';
6
7
// Import the slider data
8
import sliderData from './data/slider.data.json';
9
10
// Import the slide data type
11
import {SlideData} from './components/Slider/Slide';
12
13
const App = () => {
14
return (
15
<div className="app">
16
<Slider
17
slides={sliderData.slides as SlideData[]}
18
heading={sliderData.heading}
19
/>
20
</div>
21
);
22
};
23
24
export default App;

Now, we can run the command npm run dev to run the application.

Terminal window
npm run dev

After running the command, you will see the slider component in the home page:

Slider component

Testing the Slider Component

To start testing the slider component, we will need to install some dependencies. Run the following command:

Terminal window
npm install --save-dev @testing-library/jest-dom @testing-library/react @types/jest jest ts-jest

After installing the dependencies, we will add the jest at package.json:

package.json
1
{
2
...,
3
"jest": {
4
"transform": {
5
"^.+\\.tsx$": "ts-jest",
6
"^.+\\.ts$": "ts-jest"
7
},
8
"testEnvironment": "jest-environment-jsdom"
9
}
10
}

Now that we have the slider component, we can test it. We will use the slider component in the home page. Open the src/components/Icons/RightArrowIcon.test.tsx file and add the following code:

src/components/Icons/RightArrowIcon.test.tsx
1
import React from 'react';
2
import '@testing-library/jest-dom';
3
import {render} from '@testing-library/react';
4
import RightArrowIcon from './RightArrowIcon';
5
6
describe('RightArrowIcon', () => {
7
test('renders', () => {
8
const {container} = render(<RightArrowIcon />);
9
expect(container.firstChild).toBeInTheDocument();
10
});
11
12
test('has correct fill', () => {
13
const {container} = render(<RightArrowIcon fill="red" />);
14
expect(container.firstChild).toHaveAttribute('fill', 'red');
15
});
16
17
test('has correct size', () => {
18
const {container} = render(<RightArrowIcon size="24" />);
19
expect(container.firstChild).toHaveAttribute('width', '24');
20
expect(container.firstChild).toHaveAttribute('height', '24');
21
});
22
});

Now, we will add the slider component to the home page. Open the SliderControl.test.tsx file and add the following code:

src/components/SliderControl.test.tsx
1
import React from 'react';
2
import '@testing-library/jest-dom';
3
import {render, fireEvent} from '@testing-library/react';
4
import SliderControl from './SliderControl';
5
6
describe('SliderControl component', () => {
7
it('should render the correct title and type', () => {
8
const handleClick = jest.fn();
9
const {getByTitle} = render(
10
<SliderControl
11
type="prev"
12
title="Previous slide"
13
handleClick={handleClick}
14
/>,
15
);
16
17
expect(getByTitle('Previous slide')).toBeInTheDocument();
18
expect(getByTitle('Previous slide')).toHaveClass('btn--prev');
19
});
20
21
it('should call the handleClick function when clicked', () => {
22
const handleClick = jest.fn();
23
const {getByTitle} = render(
24
<SliderControl
25
type="next"
26
title="Next slide"
27
handleClick={handleClick}
28
/>,
29
);
30
31
fireEvent.click(getByTitle('Next slide'));
32
expect(handleClick).toHaveBeenCalled();
33
});
34
});

Now, we will add the slider component to the home page. Open the Slide.test.tsx file and add the following code:

src/components/Slide.test.tsx
1
import React from 'react';
2
import '@testing-library/jest-dom';
3
import {render, fireEvent} from '@testing-library/react';
4
import Slide, {SlideData} from './Slide';
5
6
const slideData: SlideData = {
7
index: 0,
8
src: 'test-image.jpg',
9
headline: 'Test Headline',
10
direction: 'Test Direction',
11
tags: [{name: 'Tag 1'}, {name: 'Tag 2'}],
12
links: [
13
{name: 'Link 1', url: 'http://test.com'},
14
{name: 'Link 2', url: 'http://test2.com'},
15
],
16
};
17
18
describe('Slide component', () => {
19
test('renders with slide data', () => {
20
const {getByText, getByAltText} = render(
21
<Slide
22
slide={slideData}
23
index={0}
24
current={0}
25
handleSlideClick={jest.fn()}
26
/>,
27
);
28
expect(getByAltText('Test Headline')).toBeInTheDocument();
29
expect(getByText('Test Headline')).toBeInTheDocument();
30
expect(getByText('Test Direction')).toBeInTheDocument();
31
expect(getByText('Tags:')).toBeInTheDocument();
32
expect(getByText('Tag 1')).toBeInTheDocument();
33
expect(getByText('Tag 2')).toBeInTheDocument();
34
expect(getByText('Link 1')).toHaveAttribute('href', 'http://test.com');
35
expect(getByText('Link 2')).toHaveAttribute('href', 'http://test2.com');
36
});
37
38
test('adds slide--current class when current prop matches index prop', () => {
39
const {container} = render(
40
<Slide
41
slide={slideData}
42
index={0}
43
current={0}
44
handleSlideClick={jest.fn()}
45
/>,
46
);
47
expect(container.firstChild).toHaveClass('slide slide--current');
48
});
49
50
test('adds slide--next class when current prop is 1 less than index prop', () => {
51
const {container} = render(
52
<Slide
53
slide={slideData}
54
index={1}
55
current={0}
56
handleSlideClick={jest.fn()}
57
/>,
58
);
59
expect(container.firstChild).toHaveClass('slide slide--next');
60
});
61
62
test('adds slide--previous class when current prop is 1 more than index prop', () => {
63
const {container} = render(
64
<Slide
65
slide={slideData}
66
index={0}
67
current={1}
68
handleSlideClick={jest.fn()}
69
/>,
70
);
71
expect(container.firstChild).toHaveClass('slide slide--previous');
72
});
73
74
test('calls handleSlideClick function when clicked', () => {
75
const mockHandleSlideClick = jest.fn();
76
const {container} = render(
77
<Slide
78
slide={slideData}
79
index={0}
80
current={0}
81
handleSlideClick={mockHandleSlideClick}
82
/>,
83
);
84
fireEvent.click(container.firstChild!);
85
expect(mockHandleSlideClick).toHaveBeenCalled();
86
});
87
});

Now, we will add the slider component to the home page. Open the Slider.test.tsx file and add the following code:

src/components/Slider.test.tsx
1
import React from 'react';
2
import '@testing-library/jest-dom';
3
import {render, fireEvent} from '@testing-library/react';
4
import Slider from './Slider';
5
import {SlideData} from './Slide';
6
7
describe('Slider', () => {
8
const slides = [
9
{
10
src: 'img1.jpg',
11
headline: 'Test Headline',
12
direction: 'Test Direction',
13
tags: [{name: 'Tag 1'}, {name: 'Tag 2'}],
14
links: [
15
{name: 'Link 1', url: 'http://test.com'},
16
{name: 'Link 2', url: 'http://test2.com'},
17
],
18
},
19
{
20
src: 'img2.jpg',
21
headline: 'Test Headline 2',
22
direction: 'Test Direction 2',
23
tags: [{name: 'Tag 3'}, {name: 'Tag 4'}],
24
links: [
25
{name: 'Link 3', url: 'http://test3.com'},
26
{name: 'Link 4', url: 'http://test4.com'},
27
],
28
},
29
{
30
src: 'img3.jpg',
31
headline: 'Test Headline 3',
32
direction: 'Test Direction 3',
33
tags: [{name: 'Tag 5'}, {name: 'Tag 6'}],
34
links: [
35
{name: 'Link 5', url: 'http://test5.com'},
36
{name: 'Link 6', url: 'http://test6.com'},
37
],
38
},
39
] as SlideData[];
40
41
it('should render a slider with slides and controls', () => {
42
const {getByText, getAllByRole} = render(
43
<Slider slides={slides} heading={'Test Slider'} />,
44
);
45
46
expect(getByText('Test Slider')).toBeInTheDocument();
47
expect(getAllByRole('img').length).toBe(3);
48
expect(getAllByRole('button').length).toBe(2);
49
});
50
51
it('should go to the next slide when clicking the "next" control', () => {
52
const {getAllByRole} = render(
53
<Slider slides={slides} heading={'Test Slider'} />,
54
);
55
const nextButton = getAllByRole('button')[1];
56
57
fireEvent.click(nextButton);
58
expect(getAllByRole('img')[1]).toHaveAttribute('alt', 'Test Headline 2');
59
});
60
61
it('should go to the previous slide when clicking the "previous" control', () => {
62
const {getAllByRole} = render(
63
<Slider slides={slides} heading={'Test Slider'} />,
64
);
65
const previousButton = getAllByRole('button')[0];
66
67
fireEvent.click(previousButton);
68
expect(getAllByRole('img')[2]).toHaveAttribute('alt', 'Test Headline 3');
69
});
70
71
it('should go to the clicked slide when clicking a slide', () => {
72
const {getAllByRole} = render(
73
<Slider slides={slides} heading={'Test Slider'} />,
74
);
75
const slide = getAllByRole('img')[1];
76
77
fireEvent.click(slide);
78
expect(getAllByRole('img')[1]).toHaveAttribute('alt', 'Test Headline 2');
79
});
80
});

After adding the tests, we will run the tests and see if they pass, but before that, we will add the scripts at package.json file:

package.json
1
{
2
...,
3
"scripts": {
4
...,
5
"test": "jest",
6
"test:coverage": "jest --coverage"
7
},
8
...
9
}

Now, we will run the tests:

Terminal window
npm run test

Conclusion

In this tutorial, we’ve walked through the process of building a customizable image slider in React using hooks, SCSS, and TypeScript. We started by setting up our project, creating a slider component, and implementing basic functionality to switch between images. We then added options to customize the behavior and appearance of the slider, such as Wloop, and navigation buttons.

Along the way, we learned about key React concepts such as props, state, and useEffect, as well as how to use hooks to manage component state and effects. We also saw how to use SCSS to write more expressive and reusable CSS styles, and how to leverage TypeScript to add type safety and improve code readability.

We hope this tutorial has been useful and informative, and that you now feel more comfortable building React applications with hooks, SCSS, and TypeScript. Happy coding!

Source Code

You can find the source code for this tutorial on GitHub.

SOURCE CODE

Live Demo

You can find the live demo for this tutorial on Vercel.

LIVE DEMO

Related Posts

Check out some of our other posts

Setup Nextjs Tailwind CSS Styled Components with TypeScript

Setup Nextjs Tailwind CSS Styled Components with TypeScript

Introduction In this post, we will setup Nextjs Tailwind CSS Styled Components with TypeScript, and we will use the following tools:Nextjs Tailwind CSS Styled Components TypeScript

read more
React With Redux Toolkit

React With Redux Toolkit

Prerequisites This post assumes that you have a basic understanding of React and Redux. and it will be better if you have some experience with React Hooks like useReducer. Introduction Now

read more
Run TypeScript Without Compiling

Run TypeScript Without Compiling

Introduction In this post, I will show you how to run TypeScript without compiling it to JavaScript. This is useful for debugging and testing. In this post, I will show you how to do it. Setu

read more
Introduction to Spring Boot Framework

Introduction to Spring Boot Framework

Introduction For creating web apps and microservices, many developers utilize the Spring Boot framework. The fact that it is built on top of the Spring Framework and offers a number of advantages

read more
RESTful API vs. GraphQL: Which API is the Right Choice for Your Project?

RESTful API vs. GraphQL: Which API is the Right Choice for Your Project?

TL;DR When deciding between RESTful and GraphQL APIs for a data analysis and display application, it is important to consider the advantages and disadvantages of each. RESTful APIs have been arou

read more
TypeScript vs. JSDoc: Exploring the Pros and Cons of Static Type Checking in JavaScript

TypeScript vs. JSDoc: Exploring the Pros and Cons of Static Type Checking in JavaScript

TL;DRTypeScript and JSDoc are two tools for static type checking in JavaScript. TypeScript offers a comprehensive type system, advanced features, and strict type checking. JSDoc provides l

read more