A customer reviews carousel in HTML, CSS, and JavaScript

Muhammad Saqib Ilyas
14 min readSep 30, 2023

--

Customer review sections are an important part of many websites. They showcase customer testimonials. These are commonly implemented as a carousel, where the user can browse through various reviews by pressing “next”, and “previous” buttons.

Creating this carousel is an interesting exercise in HTML, CSS, and JavaScript. In this blog, we’ll create such a carousel. So, let’s get started.

Here’s what we’d like our product to look like:

The HTML

We place the essential elements in an HTML file:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Customer review carousel</title>
<!-- font-awesome -->
<script src="https://kit.fontawesome.com/**********.js" crossorigin="anonymous"></script>

<!-- styles -->
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Customer reviews</h1>
<!-- javascript -->
<div id='loader'>
<i class="fa-solid fa-spinner fa-spin"></i>
</div>
<section class="review">
<img id='image'>
<div id='name'></div>
<div id='profession'></div>
<div id='review'></div>
<nav>
<button id='backward'><i class="fa-solid fa-chevron-left"></i></button>
<button id='forward'><i class="fa-solid fa-chevron-right"></i></button>
</nav>
</section>
<script src="app.js"></script>
</body>
</html>

In the head section, we link our fontawesome kit. I’ve intentionally hidden part of my kit URL. We link to our CSS file, as well, in the head section.

In the body section, we start with a heading. We follow it up with a div element that we’ll use to show a “loading” spinner icon. We have next asection element that houses the customer review. It contains an img element for the customer’s image, and div elements for the customer’s name, profession, and their review. Up next are the browsing icons in a nav element. We use the fontawesome chevron icons, here.

The CSS

We place the elements in a somewhat pleasant way with the following CSS:

*,
::after,
::before {
margin: 0;
padding: 0;
box-sizing: border-box;
}

#loader {
display: flex;
height: 100vh;
justify-content: center;
align-items: center;
}

.review {
visibility: hidden;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}

.review img {
clip-path: circle();
}

#name {
font-family: "Roboto", sans-serif;
font-size: 1.5rem;
}

#profession {
font-size: 1rem;
font-style: italic;
}

#review {
font-family: "Open Sans", sans-serif;
padding: 2rem;
}

We start with a CSS reset by setting the margin, padding and box-sizing properties for all HTML elements. This helps us start with a consistent styling on all web browsers.

In order to vertically and horizontally center the “loading” icon, we use CSS flex and set the align-items, and justify-contents properties to center. To have the “loading” animation occupy the entire screen, we set its height property to 100vh, which means 100% of the view height.

In the review section, we want to place the items (picture, name, profession, and review) on separate rows, so we use flexbox with flex-direction set to column. Also, we want these horizontally centered, so we use justify-contents, and align-items set to center. We set visibility to hidden so that when the page loads, this part of the page does not show up. We need API calls to fetch the customer images, so until the image loads, the review shouldn’t appear.

In order to give the customer images a circular shape, we use clip-path with a value of circle(). The rest of the styles are for font and font size selection.

Loading and displaying customer images

For the customer images, we can use the randomuser API.

const image = document.getElementById('image')
const name = document.getElementById('name')
const profession = document.getElementById('profession')
const message = document.getElementById('review')
const forward = document.getElementById('forward')
const backward = document.getElementById('backward')
const loader = document.getElementById('loader')

const length = 20
const imageurl = 'https://randomuser.me/api/?results=' + length

fetch(imageurl).then(response => {
if (response.ok) {
return response.json()
}
else {
throw new Error('API request failed')
}
}).then(data => {
image.src = data.results[0].picture['large']
name.textContent = data.results[0].name.last + ', ' + data.results[0].name.first
profession.textContent = 'Web Developer'
loader.style.visibility = 'hidden';
loader.style.height = '0'
reviewSection.style.visibility = 'visible'
}).catch(error => console.log(error))

First, we acquire objects corresponding to the HTML elements in the customer review section, so that we may display the results using JavaScript. On to fetching the customer images.

We may fetch customer photos one at a time. However, API requests are expensive, and the randomuser API offers the ability to fetch multiple results at once. So, it is best to fetch multiple customer images at once, store them locally, and cycle through them as the user presses the “next” and “previous” buttons.

Let’s decide to show 20 customer reviews. So, we initialize a variable length to that value. We store the API url in a string. Notice the query parameter for the number of results being stored with the string.

We use the fetch API to make the HTTP request for the images. This call returns a Promise. In the then() part, which represents what happens when the API call returns, we check if the response status was OK. If so, we return its JSON representation. Otherwise, we throw an Error notifying of an HTTP errors that occurred.

The response.json() call also returns a promise. If you do a console.log(data) in its then() block, you will see that the response has an elaborate structure shown below:

results: Array(20)
0:
cell: "(841) 727-4510"
dob: {date: '1992-02-23T17:39:33.215Z', age: 31}
email: "anne.stanley@example.com"
gender: "female"
id: {name: 'SSN', value: '781-26-8212'}
location: {street: {…}, city: 'Santa Clara', state: 'Maryland', country: 'United States', postcode: 82200, …}
login: {uuid: 'd0b80c6c-087e-412c-b2ca-3d2c9d5cf231', username: 'smallostrich304', password: 'buffy1', salt: 'aHE3fhXR', md5: '1b0bbe78e9255d0c938fb68800b9d265', …}
name: {title: 'Miss', first: 'Anne', last: 'Stanley'}
nat: "US"
phone: "(683) 891-2092"
picture: {large: 'https://randomuser.me/api/portraits/women/72.jpg', medium: 'https://randomuser.me/api/portraits/med/women/72.jpg', thumbnail: 'https://randomuser.me/api/portraits/thumb/women/72.jpg'}
registered: {date: '2005-07-08T17:06:36.325Z', age: 18}

Note that it has a results array. Each element in the response array has a host of fields. The prominent ones are picture, which is an object with keys named large, medium, and thumbnail. Also useful is the name field, which has keys named first and last.

In the (second)then() block, we set the src attribute of the img element to the large image returned by the API. At the moment, let’s hard-code the array index to 0.

For now, we hard-code the profession to “Web Developer.” We’ll work on randomly generating it, later.

In order to hide the “loading” animation, we set its visibility to hidden. We also set its height to 0, so that it doesn’t occupy any space. Since the review section is ready to be shown, we set its visibility to visible. Finally, the catch() block catches and displays any error that may occur anywhere in the Promise chain.

We should have something like this, so far:

A view of our application so far

Generating random customer reviews

For the customer reviews, we can use some API that generates random text. For example, we could use generative AI with a prompt such as “Generate a random customer review for a fast food joint”. I experimented with DeepAI. The use of such services typically requires getting a subscription. For this project, we can generate some random “lorem ipsum” text.

Let’s start by generating a random sentence:

const words = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', 'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua'];

const minWordsPerSentence = 5
const maxWordsPerSentence = 15

function generateRandomSentence(numWords) {
let sentence = ''
for (let i = 0 ; i < numWords ; i++) {
const randIndex = Math.floor(Math.random()*words.length)
sentence += words[randIndex]
if(i < numWords - 1) {
sentence += ' '
}
}
sentence += '.'
return sentence
}

We store a bunch of “lorem ipsum” words in an array. We don’t want the sentences to be fixed number of words. We don’t want sentences to be too short, either. We define two variables to define the minimum and maximum number of words per sentence. We define a function generateRandomSentence() to return a random sentence.

The generateRandomSentence() function accepts an argument representing the number of words that should be in the randomly generated word. We initialize an empty string to hold the randomly generated sentence. We run a for loop for each random word in the sentence. We generate a random integer between 0 and n-1, where n is the length of the words array. We pick the word from the randomly drawn index, and append it to the string sentence. If we have more words to generate, we append a space to the string. At the end, we append a period to end the sentence, and return the randomly generated string.

Each review should have one or more sentences. So, we’ll use the above code repeatedly to generate a complete review.

const minSentences = 1
const maxSentences = 5

function generateRandomReview() {
let review = ''
const numSentences = Math.floor(Math.random()*(maxSentences - minSentences + 1) + minSentences)
for (let i = 0 ; i < numSentences ; i++) {
const numWords = Math.floor(Math.random()*(maxWordsPerSentence - minWordsPerSentence + 1) + minWordsPerSentence)
review += generateRandomSentence(numWords)
if (i < numSentences - 1) {
review += ' '
}
}
return review
}

Let’s say we want a review to have anywhere between 1 and 5 sentences. We define variables to store these limits. We define a function generateRandomReview() to generate one whole review. We initialize an empty string to hold the review. We generate a random integer between 1 and 5, for the number of sentences in this review. We then run a for loop that many times to generate a random sentence. For each sentence, we generate a random number to represent the number of words in that sentence, and call our generateRandomSentence() function. We append the random sentence to the review string. If there are more sentences to generate, we append a space to this string so that there’s a space between the period at the end of the current sentence, and the first letter of the next sentence. Finally, once all sentences are appended to the review, we return it.

Since we fetched 20 customer images, we need 20 reviews to go with it. So, we call the above function 20 times:

let reviews = []

for (let i = 0 ; i < length ; i++) {
reviews.push(generateRandomReview())
}

Now, we can use the reviews array in the fetch API code block, by inserting a statement like message.textContent = reviews[0]. Here’s the complete code, so far:

const length = 20
const imageurl = 'https://randomuser.me/api?results=' + length + '&gender=female&nat=US'

const image = document.getElementById('image')
const name = document.getElementById('name')
const profession = document.getElementById('profession')
const message = document.getElementById('review')
const forward = document.getElementById('forward')
const backward = document.getElementById('backward')
const loader = document.getElementById('loader')
const reviewSection = document.querySelector('.review')

const words = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', 'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua'];
let reviews = []

const minWordsPerSentence = 5
const maxWordsPerSentence = 15

const minSentences = 1
const maxSentences = 5

function generateRandomSentence(numWords) {
let sentence = ''
for (let i = 0 ; i < numWords ; i++) {
const randIndex = Math.floor(Math.random()*words.length)
sentence += words[randIndex]
if(i < numWords - 1) {
sentence += ' '
}
}
sentence += '.'
return sentence
}

function generateRandomReview() {
let review = ''
const numSentences = Math.floor(Math.random()*(maxSentences - minSentences + 1) + minSentences)
for (let i = 0 ; i < numSentences ; i++) {
const numWords = Math.floor(Math.random()*(maxWordsPerSentence - minWordsPerSentence + 1) + minWordsPerSentence)
review += generateRandomSentence(numWords)
if (i < numSentences - 1) {
review += ' '
}
}
return review
}

for (let i = 0 ; i < length ; i++) {
reviews.push(generateRandomReview())
}

fetch(imageurl).then(response => {
if (response.ok) {
return response.json()
}
else {
throw new Error('API request failed')
}
}).then(data => {
image.src = data.results[0].picture['large']
name.textContent = data.results[0].name.last + ', ' + data.results[0].name.first
profession.textContent = 'Web Developer'
loader.style.visibility = 'hidden';
loader.style.height = '0'
message.textContent = reviews[0]
reviewSection.style.visibility = 'visible'
}).catch(error => console.log(error))

Now, you should be able to see random “lorem ipsum” text in the customer review like the following:

A randomly generated customer review

Navigation

Now, let’s handle the button clicks to cycle through the customer reviews. A function that displays the data for a given index would be handy:

function displayData(index){
image.src = imagesArray[currentIndex].picture['large']
name.textContent = imagesArray[currentIndex].name.last + ', ' + imagesArray[currentIndex].name.first
profession.textContent = 'Web Developer'
message.textContent = reviews[currentIndex]
}

This would simplify our fetch() API code as well. We need to store the results from the fetch() API call in a variable named imagesArray, though.

let currentIndex = 0
let imagesArray

fetch(imageurl).then(response => {
if (response.ok) {
return response.json()
}
else {
throw new Error('API request failed')
}
}).then(data => {
imagesArray = data.results
displayData(currentIndex)
loader.style.visibility = 'hidden';
loader.style.height = '0'
reviewSection.style.visibility = 'visible'
}).catch(error => console.log(error))

function displayData(index){
image.src = imagesArray[currentIndex].picture['large']
name.textContent = imagesArray[currentIndex].name.last + ', ' + imagesArray[currentIndex].name.first
profession.textContent = 'Web Developer'
message.textContent = reviews[currentIndex]
}

Now, what we need to do to handle the “next” and “previous” button clicks is to increment or decrement the value of currentIndex.

forward.addEventListener('click', () => {
currentIndex++
displayData(currentIndex)
})

Now, the “next” button cycles forward through the reviews. The catch is that when currentIndex exceeds the size of the reviews array, you get an error. So, we should not increment currentIndex beyond 20. We should also disable the “next” button once the value of currentIndex becomes 19.

Let’s change the implementation to:

forward.addEventListener('click', () => {
if (currentIndex < 19) {
currentIndex++
displayData(currentIndex)
}
if (currentIndex === 19) {
forward.disabled = true
}
})

We check if the value of currentIndex is less than 19, we go ahead and increment it and display the next review. Next, if the value of currentIndex has reached 19, there’s no next review left to show, so we set the disabled property of the “next” button to true.

Let’s repeat this with the previous button:

backward.addEventListener('click', () => {
if (currentIndex > 0) {
currentIndex--
displayData(currentIndex)
}
if (currentIndex === 0) {
backward.disabled = true
}
})

A few realizations at this point:

  • The “previous” button should have been disabled to start with
  • After you’ve browsed through the 20 reviews, and hit the “previous” button, the “next” button should become enabled
  • Once you press the “next” button, the “previous” button should become enabled.

The first issue can be handled in HTML by changing the button markup to:

<button id='backward' disabled><i class="fa-solid fa-chevron-left"></i></button>
<button id='forward'><i class="fa-solid fa-chevron-right"></i></button>

For the other two issues, perhaps we should write a function that enables or disables both of the buttons based on the value of currentIndex. Here’s the updated code:

forward.addEventListener('click', () => {
if (currentIndex < 19) {
currentIndex++
displayData(currentIndex)
}
toggleButtons()
})

backward.addEventListener('click', () => {
if (currentIndex > 0) {
currentIndex--
displayData(currentIndex)
}
toggleButtons()
})

function toggleButtons() {
forward.disabled = currentIndex < 19 ? false : true
backward.disabled = currentIndex > 0 ? false : true
}

In the toggleButtons() function, we use the ternary operator to set the value of the disabled property of both the buttons based on the value of the currentIndex variable. For instance, if currentIndex is less than 19, then the disabled property for the “next” button should be false, otherwise, it should be true.

One annoying thing that happens is that as you navigate through the reviews, the buttons move vertically depending on the size of the review text. One way to fix it is to set overflow property for the corresponding div to auto, and set the height to a certain value so that you get scroll bars if the review text can’t fit in the available area, while the buttons stay in their place. Let’s add the following to the CSS:

:root {
--review-height: 120px;
}

#review {
font-family: "Open Sans", sans-serif;
padding: 2rem;
overflow: auto;
height: var(--review-height);
}

We use a CSS variable to hold the pre-determined height, so that we can change it conveniently.

Picking random occupations

I picked up a JavaScript array of occupations from a GitHub repository. We can store the occupations in an array named professions. Then, while generating random reviews, we can pick a random occupation at the same time.

const professions = [
"Academic librarian",
"Accountant",
"Accounting technician",
"Actuary",
// Many more
"Waste disposal officer",
"Water conservation officer",
"Water engineer",
"Web designer",
"Web developer",
"Welfare rights adviser",
"Writer",
"Youth worker"
];

for (let i = 0 ; i < length ; i++) {
reviews.push({'occupation': professions[Math.floor(Math.random()*professions.length)], 'review': generateRandomReview()})
}

function displayData(index){
image.src = imagesArray[currentIndex].picture['large']
name.textContent = imagesArray[currentIndex].name.last + ', ' + imagesArray[currentIndex].name.first
profession.textContent = reviews[currentIndex].occupation
message.textContent = reviews[currentIndex].review
}

We turn the review into an object, with two keys: occupation, and review. Later, when displaying the review, we access the corresponding values with the respective key.

Final touches and complete code

Finally, we add a box shadow, and some background color to the review. You may download the complete code from this repository. Here’s the HTML:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Starter</title>
<!-- font-awesome -->
<script src="https://kit.fontawesome.com/c9d2b38a42.js" crossorigin="anonymous"></script>

<!-- styles -->
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Customer reviews</h1>
<!-- javascript -->
<div id='loader'>
<i class="fa-solid fa-spinner fa-spin"></i>
</div>
<section class="review">
<img id='image'>
<div id='name'></div>
<div id='profession'></div>
<div id='review'></div>
<nav>
<button id='backward' disabled><i class="fa-solid fa-chevron-left"></i></button>
<button id='forward'><i class="fa-solid fa-chevron-right"></i></button>
</nav>
</section>
<script src="app.js"></script>
</body>
</html>

The CSS with some more constants move into CSS variables:

:root {
--review-height: 120px;
--clr-light-gray: #f8f8f8;
--box-margin: 20px;
--review-padding: 2px 0;
--review-radius: 10px;
--box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.5);
--font-size: 0.875rem;
--line-height: 1.5;
}

*,
::after,
::before {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--ff-secondary);
background: var(--clr-grey-10);
color: var(--clr-grey-1);
line-height: var(--line-height);
font-size: var(--font-size)
}

#loader {
display: flex;
height: 100vh;
justify-content: center;
align-items: center;

}
.review {
visibility: hidden;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
box-shadow: var(--box-shadow);
background-color: var(--clr-light-gray);
border-radius: var(--review-radius);
padding: var(--review-padding);
margin: var(--box-margin);
}

.review img {
clip-path: circle();
}

#name {
font-family: "Roboto", sans-serif;
font-size: 1.5rem;
}

#profession {
font-size: 1rem;
font-style: italic;
}

#review {
font-family: "Open Sans", sans-serif;
padding: 2rem;
overflow: auto;
height: var(--review-height);
}

h1 {
text-align: center;
}

The JavaScript with the array values snipped for convenience:

const length = 20
const imageurl = 'https://randomuser.me/api?results=' + length + '&gender=female&nat=US'

const professions = [
"Academic librarian",
"Accountant",
"Accounting technician",
"Actuary",
// More values
"Web designer",
"Web developer",
"Welfare rights adviser",
"Writer",
"Youth worker"
];

const image = document.getElementById('image')
const name = document.getElementById('name')
const profession = document.getElementById('profession')
const message = document.getElementById('review')
const forward = document.getElementById('forward')
const backward = document.getElementById('backward')
const loader = document.getElementById('loader')
const reviewSection = document.querySelector('.review')

const words = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', 'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua'];
let reviews = []

const minWordsPerSentence = 5
const maxWordsPerSentence = 15

const minSentences = 1
const maxSentences = 5

function generateRandomSentence(numWords) {
let sentence = ''
for (let i = 0 ; i < numWords ; i++) {
const randIndex = Math.floor(Math.random()*words.length)
sentence += words[randIndex]
if(i < numWords - 1) {
sentence += ' '
}
}
sentence += '.'
return sentence
}

function generateRandomReview() {
let review = ''
const numSentences = Math.floor(Math.random()*(maxSentences - minSentences + 1) + minSentences)
for (let i = 0 ; i < numSentences ; i++) {
const numWords = Math.floor(Math.random()*(maxWordsPerSentence - minWordsPerSentence + 1) + minWordsPerSentence)
review += generateRandomSentence(numWords)
if (i < numSentences - 1) {
review += ' '
}
}
return review
}

for (let i = 0 ; i < length ; i++) {
reviews.push({'occupation': professions[Math.floor(Math.random()*professions.length)], 'review': generateRandomReview()})
}

let currentIndex = 0
let imagesArray

fetch(imageurl).then(response => {
if (response.ok) {
return response.json()
}
else {
throw new Error('API request failed')
}
}).then(data => {
imagesArray = data.results
displayData(currentIndex)
loader.style.visibility = 'hidden';
loader.style.height = '0'
reviewSection.style.visibility = 'visible'
}).catch(error => console.log(error))

function displayData(index){
image.src = imagesArray[currentIndex].picture['large']
name.textContent = imagesArray[currentIndex].name.last + ', ' + imagesArray[currentIndex].name.first
profession.textContent = reviews[currentIndex].occupation
message.textContent = reviews[currentIndex].review
}

forward.addEventListener('click', () => {
if (currentIndex < 19) {
currentIndex++
displayData(currentIndex)
}
toggleButtons()
})

backward.addEventListener('click', () => {
if (currentIndex > 0) {
currentIndex--
displayData(currentIndex)
}
toggleButtons()
})

function toggleButtons() {

forward.disabled = currentIndex < 19 ? false : true
backward.disabled = currentIndex > 0 ? false : true
}

In the code repository, you’ll find commented out code that I wrote for accessing the DeepAI API. In case you want to play with an API for the reviews, you might find that a useful start.

--

--

Muhammad Saqib Ilyas

A computer science teacher by profession. I love teaching and learning programming. I like to write about frontend development, and coding interview preparation