A customer reviews carousel in HTML, CSS, and JavaScript
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:
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:
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.