Hello, Today we'll see, how we can easily create a netflix clone using HTML, CSS and JS only. No other library. We'll also use TMDB API to fetch real data from their database.
Netflix Clone, we all use netflix in our day to day life. And if you are just starting with web development. This project can be a good practice project for you. This netflix clone is a dynamic site and has everything you need for fullstack development practice. It runs on a Node.js server. It uses TMDB API for all data.
Features
- Looks similar to Netflix.
- Dynamic site run on Node.js server.
- All data is coming from TMDB API.
- Dedicated Dynamic Movies Info page.
- Has movie trailers, and recommendations.
- Has smooth card slider effect.
To see demo or you want full coding tutorial video. You can watch the tutorial below.
Video Tutorial
I appreciate if you can support me by subscribing my youtube channel.
So, without wasting more time let's see how to code this.
Code
As this is a node.js web app. We need NPM and Node.js in order to start with, so make sure you have them installed in your system.
So let's start with its folder structure.
Folder Structure.
This is our folder structure.
NPM Init
Let's start with initializing NPM. So outside public
folder, In your root
directory, open Command Prompt or terminal. And execute. npm init
It will ask you for some details. You can press enter to have default project details. After executing npm init
you should see a package.json
file.
Great Now install Some libraries that we need in order to create a server.
Installing Libraries
After creating package.json
file. Run this command.
npm i express.js nodemon
i
- means install.
express.js
- is a library we need to create server.
nodemon
- is a library which allow you to run server seamlessly even after making changes to the server.
After installation complete, you should be able to see node_modules
folder in your root
directory.
Now open package.json
file in your text editor. And edit it a little bit.
Delete
"test"
cmd from"scripts"
object. And add new cmd called"start"
and set it value to"nodemon server.js"
.
Server.js
After editing package.json
create JS file server.js
in the root
directory.
And write this in server.js
.
const express = require('express');
const path = require('path');
let initial_path = path.join(__dirname, "public");
let app = express();
app.use(express.static(initial_path));
app.get('/', (req, res) => {
res.sendFile(path.join(initial_path, "index.html"));
})
app.listen(3000, () => {
console.log('listening on port 3000......');
})
Explanation
In the top, we are using require
method to import library so that we can use it in this file. We are importing two libraries express
and path
.
path
library is used to track paths.
After done importing libraries. We are setting a variable app
equal to express()
, which enable all the server related features to our app
variable. And we also have initial_path
which is holding our public
folder path.
After that we have, app.use()
which is used as a middle ware And inside this we have express.static()
which allow us to set our static directory path. In this case we are setting our public
folder as a static path, because our HTML
files are inside that folder.
app.get('/')
is a listener, And in this case it is listening for a GET
request to our root /
path. And whenever we get any GET
request on /
. We will serve them index.html
file. That's what res.sendFile()
do.
And the last block of our server.js
is app.listen
which is used to add a server's listening port. In this case, we set it to 3000
. So our server will run on localhost:3000
. Not any other port.
Now in your terminal or cmd prompt. Run npm start
to start the server. And, open your browser to localhost:3000
. You'll be able to see index.html
file.
So up until now we have created our server and successfully serving our index.html
file to /
path.
So let's do some front end work here. Now
Home page.
So for our Home page, we will use these files. index.html
, style.css
, home.js
, api.js
, scroll.js
.
Let's start from index.html
file. Start typing basic HTML structure. And after that link style.css
file. And let's create navbar first.
<!-- navbar -->
<nav class="navbar">
<img src="img/logo.png" class="logo" alt="">
<div class="join-box">
<p class="join-msg">unlimited tv shows & movies</p>
<button class="btn join-btn">join now</button>
<button class="btn">sign in</button>
</div>
</nav>
Make sure your server is running, if it is not then run
npm start
in your terminal.
Output
All the CSS properties I'll use are easy to understand. So i'll only explain you JS only. But if you have doubt in any part. Even in CSS. Feel free to ask me in discussions.
Now style the navbar
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
body{
width: 100%;
position: relative;
background: #181818;
font-family: 'roboto', sans-serif;
}
.navbar{
width: 100%;
height: 60px;
position: fixed;
top: 0;
z-index: 9;
background: #000;
padding: 0 2.5vw;
display: flex;
align-items: center;
}
.logo{
height: 60%;
}
.join-box{
width: fit-content;
display: flex;
justify-content: center;
align-items: center;
height: auto;
margin-left: auto;
}
.join-msg{
color: #fff;
text-transform: uppercase;
}
.btn{
border: 1px solid #fff;
border-radius: 2px;
background: none;
color: #fff;
height: 35px;
padding: 0 10px;
margin-left: 10px;
text-transform: uppercase;
cursor: pointer;
}
.join-btn{
background: #dd0e15;
border-color: #dd0e15;
}
Output
<!-- main section -->
<header class="main">
<h1 class="heading">movies</h1>
<p class="info">Movies move us like nothing else can, whether they're scary, funny, dramatic, romantic or anywhere in-between. So many titles, so much to experience.</p>
</header>
And style it
.main{
position: relative;
margin-top: 60px;
width: 100%;
padding: 40px 2.5vw;
color: #fff;
}
.heading{
text-transform: capitalize;
font-weight: 900;
font-size: 50px;
}
.info{
width: 50%;
font-size: 20px;
margin-top: 10px;
}
And we have to create a movie list element inside .main
element, this will hold our same genres movie.
<div class="movie-list">
<button class="pre-btn"><img src="img/pre.png" alt=""></button>
<h1 class="movie-category">Popular movie</h1>
<div class="movie-container">
<div class="movie">
<img src="img/poster.jpg" alt="">
<p class="movie-title">movie name</p>
</div>
</div>
<button class="nxt-btn"><img src="img/nxt.png" alt=""></button>
</div>
You can see here, we have pre-btn
and nxt-btn
with them we also have a movie-card
element. Well, we will create movie card and list element all with JS but for styling purpose we are creating one card here. Just for the sake of CSS.
.movie-list{
width: 100%;
height: 250px;
margin-top: 40px;
position: relative;
}
.movie-category{
font-size: 20px;
font-weight: 500;
margin-bottom: 20px;
text-transform: capitalize;
}
.movie-container{
width: 100%;
height: 200px;
display: flex;
align-items: center;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
}
.movie-container::-webkit-scrollbar{
display: none;
}
.movie{
flex: 0 0 auto;
width: 24%;
height: 200px;
text-align: center;
margin-right: 10px;
cursor: pointer;
position: relative;
}
.movie img{
width: 100%;
height: 170px;
object-fit: cover;
}
.movie p{
text-transform: capitalize;
height: 20px;
overflow: hidden;
}
.pre-btn,
.nxt-btn{
position: absolute;
height: 200px;
top: 50%;
transform: translateY(-50%);
width: 2.5vw;
background: #181818;
border: none;
outline: none;
opacity: 0;
}
.pre-btn{
left: -2.5vw;
}
.nxt-btn{
right: -2.5vw;
}
.pre-btn img,
.nxt-btn img{
width: 20px;
height: 20px;
object-fit: contain;
}
.nxt-btn:hover,
.pre-btn:hover{
opacity: 1;
}
Output
Once we are done styling our cards. we can commit them.
<header class="main">
<h1 class="heading">movies</h1>
<p class="info">Movies move us like nothing else can, whether they're scary, funny, dramatic, romantic or anywhere in-between. So many titles, so much to experience.</p>
<!-- movie list -->
<!-- <div class="movie-list">
<button class="pre-btn"><img src="img/pre.png" alt=""></button>
<h1 class="movie-category">Popular movie</h1>
<div class="movie-container">
<div class="movie">
<img src="img/poster.jpg" alt="">
<p class="movie-title">movie name</p>
</div>
</div>
<button class="nxt-btn"><img src="img/nxt.png" alt=""></button>
</div> -->
</header>
Our main
section should look like this. As we are done with homepage.
Now add all JS files in index.html
file. As we need them now.
<script src="js/api.js"></script>
<script src="js/scroll.js"></script>
<script src="js/home.js"></script>
Make sure you add these files in exactly same order.
Now go to TMDB Official Site TO create an API key. If you don't know how to create it. Watch This.
After creating API key paste it into api.js
file
api.js
let api_key = "your api key";
And after that go to TMDB Documentation. And find these three HTTP links.
api.js
let api_key = "your api key";
let img_url = "https://image.tmdb.org/t/p/w500";
let genres_list_http = "https://api.themoviedb.org/3/genre/movie/list?";
let movie_genres_http = "https://api.themoviedb.org/3/discover/movie?";
-
img_url
- is to fetch image. Because we'll get movie image's path id. For example if we got image id as123
then the image url will behttps://image.tmdb.org/t/p/w500/123
-
genres_list_http
- is to fetch movie genres list so we don't have to fetch different genres movie manually. -
movie_genres_http
- is to fetch the movie having same genres.
After done with these HTTPs. Open home.js
file.
home.js
fetch(genres_list_http + new URLSearchParams({
api_key: api_key
}))
.then(res => res.json())
.then(data => {
data.genres.forEach(item => {
fetchMoviesListByGenres(item.id, item.name);
})
});
Explanation
Here, we are using fetch
method to genres_list_http
that we have declared in api.js
file. And using new URLSearchParams
for adding api_key
parameters to the link. And after getting res we are converting it to JSON be res.json()
and after converting it to JSON we got the fetched data. Inside that. before understanding what we are doing. First see our fetched data structure.
So as to understood the data structure. Now understand what we are doing after getting JSON data.
data.genres.forEach(item => {
fetchMoviesListByGenres(item.id, item.name);
})
As we have an array of genres, we are looping through each and every genres using forEach
method. And inside that we are calling fetchMoviesListByGenres(id, genres)
which we'll create next.
Now fetch movies with genres.
const fetchMoviesListByGenres = (id, genres) => {
fetch(movie_genres_http + new URLSearchParams({
api_key: api_key,
with_genres: id,
page: Math.floor(Math.random() * 3) + 1
}))
.then(res => res.json())
.then(data => {
makeCategoryElement(`${genres}_movies`, data.results);
})
.catch(err => console.log(err));
}
Explanation
Here we are doing the same, we are fetching data but in this case we are making request to movie_genres_http
and adding some more parameters.
with_genres
param will give us movie with only that genres for instance if our genres id if for comedy movies, then we'll only get comedy movies.
page
param will give use what of the result we want and in this case we are using Math.random()
to fetch some random page of movie result.
After getting the data, we are performing the same res.json()
to covert it into JSON. And calling makeCategoryElement(category, data)
which will create our movie categories. Same if you want you can console log the data structure.
Now create movie categories. But before that select our main
element from HTML.
const main = document.querySelector('.main');
const makeCategoryElement = (category, data) => {
main.innerHTML += `
<div class="movie-list">
<button class="pre-btn"><img src="img/pre.png" alt=""></button>
<h1 class="movie-category">${category.split("_").join(" ")}</h1>
<div class="movie-container" id="${category}">
</div>
<button class="nxt-btn"><img src="img/nxt.png" alt=""></button>
</div>
`;
makeCards(category, data);
}
Explanation
In this function, we have two arguments one is category
and second is data
. So the first thing our function is doing is adding a .movie-list
element to our main
element using innerHTML
. If you remember this we created in our HTML file but at last commented copy that code and paste it here. Make sure you use +=
not =
because we don't want to re-write its HTML.
<h1 class="movie-category">${category.split("_").join(" ")}</h1>
if you see this line. First of all we are using JS template string if you don;t use that you'll be not able to write like this. So here as we had an h1
element. we are setting it's text to our category that we got at the start of the function. But we also performing some methods here.Let's see them in detail.
for instance, assume category is equal to comedy.
-
<h1 class="movie-category">${category}</h1>
Then output will be - comdey_movies. But we don't wont_
that why we split it. -
<h1 class="movie-category">${category.split("_")}</h1>
Then it will not work because now we have an array ["comedy", "movies"]. That's why usejoin
method to join the array. -
<h1 class="movie-category">${category.split("_").join(" ")}</h1>
Then the output will be - Comedy Movies
I hope you understood this.
And then we are setting up a unique id to movie-container
element so we can add card to it later. And at the very last, we are calling makeCards(category, data)
to make cards inside that movie container element.
Now create a cards.
const makeCards = (id, data) => {
const movieContainer = document.getElementById(id);
data.forEach((item, i) => {
})
}
Explanation
Inside this function, we are selecting the movie container element on the start using that id
we got from the above function. And after that we are looping through data
using forEach
method. Inside that We are checking some condition.
if(item.backdrop_path == null){
item.backdrop_path = item.poster_path;
if(item.backdrop_path == null){
return;
}
}
This condition is checking, if we don't have movie backdrop
image path in our result set it to poster_path
and we don't have that too. Don't make the card. Sometime TMDB movie's data do not have image path in it that's why we are checking for it.
After that we have
movieContainer.innerHTML += `
<div class="movie" onclick="location.href = '/${item.id}'">
<img src="${img_url}${item.backdrop_path}" alt="">
<p class="movie-title">${item.title}</p>
</div>
`;
Here, we are using the innerHTML
method to append the card HTML structure that we already made at the start. And again here also we are using template strings. If you see we have onclick
event to movie-card
element which, in that event we are using location.href
to redirect user to movie page that we'll create next.
if(i == data.length - 1){
setTimeout(() => {
setupScrolling();
}, 100);
}
And this is checking for the last cast. when we are done creating cards. we are running setupScrolling()
function to setup slider effect. We also have to create this.
After writing this much of JS. Now we can see the output without any errors.
Output
But we haven't created our slider effect write. For that open scroll.js
file.
scroll.js
const setupScrolling = () => {
const conainter = [...document.querySelectorAll('.movie-container')];
const nxtBtn = [...document.querySelectorAll('.nxt-btn')];
const preBtn = [...document.querySelectorAll('.pre-btn')];
}
Explanation
First in this function we are selecting our containers, next buttons and previous buttons using querySelectorAll
method.
After selecting them. Inside the function type this.
conainter.forEach((item, i) => {
let containerDimensions = item.getBoundingClientRect();
let containerWidth = containerDimensions.width;
})
Here we are looping through each container element. And using getBoundingClientRect
method to get container's dimensions. And at last storing containerDimensions.width
(which of course give container width) to containerWidth
.
After that inside this for loop add this.
nxtBtn[i].addEventListener('click', () => {
item.scrollLeft += containerWidth;
})
preBtn[i].addEventListener('click', () => {
item.scrollLeft -= containerWidth;
}
Here we are selecting our nxtBtn
and preBtn
with container's index. And adding click event to them. And just performing simple maths.
After this we should be able to get our slider effect.
Our Home page is done.
Server.js
Now for about page we need to add some more codes in server.js
.
Type this before app.listen()
;
app.get('/:id', (req, res) => {
res.sendFile(path.join(initial_path, "about.html"));
})
app.use((req, res) => {
res.json("404");
})
Here, we are adding GET request listener to /:id
path. THis means anything with a single slash in front, execute the code. It will work for /123
but not for /123/12/1
. And at last we have app.use()
which again use as a middle ware and this means that if the requesting path is not the same as above paths. Execute this. Means 404
message.
After this you'll be able to redirect yourself to movie detail page by clicking on movie card.
About Page
Let's create this last page. For this link both about.css
and style.css
file so we don't have to write lot of CSS.
And copy paste the navbar here. After that create movie-info element
about.html
<!-- movie info -->
<div class="movie-info">
<div class="movie-detail">
<h1 class="movie-name">Movie Name</h1>
<p class="genres">Comedy</p>
<p class="des">Lorem ipsum dolor sit amet consectetur, adipisicing elit. In commodi incidunt odit inventore suscipit, debitis officia modi exercitationem animi nemo.</p>
<p class="starring"><span>Starring:</span></p>
</div>
</div>
And style it.
.movie-info{
width: 100%;
height: calc(100vh - 60px);
margin-top: 60px;
background-size: cover;
background-repeat: no-repeat;
}
.movie-detail{
width: 50%;
height: 100%;
background: rgb(24, 24, 24);
background: linear-gradient(90deg, rgba(24, 24, 24, 1), rgba(24, 24, 24, 0) 100%);
padding: 5vw;
display: flex;
flex-direction: column;
justify-content: flex-end;
color: #fff;
}
.movie-name{
font-size: 30px;
font-weight: 500;
}
.genres{
opacity: 0.6;
margin: 30px 0;
}
.des{
width: 70%;
line-height: 20px;
margin-bottom: 30px;
}
.starring span{
opacity: 0.6;
}
Output
Once CSS is complete you can remove all the text from the info elements making them totally empty.
<h1 class="movie-name"></h1>
<p class="genres"></p>
<p class="des"></p>
<p class="starring"><span>Starring:</span></p>
Like this.
Now create video recommendation.
<div class="trailer-container">
<h1 class="heading">Video Clip</h1>
<iframe src="" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
You can notice we have iframe
here. And this is little hard to understand so I suggest you watch this to understand video trailer better.
Style It.
.trailer-container,
.recommendations{
color: #fff;
padding: 5vw 5vw 0;
}
.heading{
font-size: 30px;
font-weight: 300;
margin-bottom: 20px;
}
iframe{
width: 400px;
height: 200px;
}
In output we'll see nothing except our movie info element and a video clip text. Because our iframe
source is empty.
Now create recommendation container.
<div class="recommendations">
<h1 class="heading">More Like This</h1>
<div class="recommendations-container">
<div class="movie">
<img src="img/poster.jpg" alt="">
<p class="movie-title">movie name</p>
</div>
</div>
</div>
CSS
.recommendations-container{
width: 100%;
display: flex;
flex-wrap: wrap;
}
.movie p{
position: absolute;
bottom: 30px;
width: 100%;
height: 30px;
line-height: 30px;
background: rgba(0, 0, 0, 0.5);
text-align: center;
opacity: 0;
}
.movie:hover p{
opacity: 1;
}
Output
As we have done styling. You can comment .movie
element. This is the same element that we have created in home page.
Add scripts to this page also. And remeber to add this is same exact order.
<script src="js/api.js"></script>
<script src="js/about.js"></script>
Now open api.js
file. And add this.
let original_img_url = "https://image.tmdb.org/t/p/original";
let movie_detail_http = "https://api.themoviedb.org/3/movie";
You can find these HTTPs from TMDB documentation.
original_img_url
- This is to fetch movie image in original resolution.
movie_detail_http
- This is to fetch details of a particular movie.
Now open about.js
. And write this.
let movie_id = location.pathname;
by location.pathname
you will be able to extract movie id from the URL. For example if the URL is localhost:3000/123
then this will return /123
which is our movie id.
After that fetch movie details using the same fetch
method. and pass the fetched data to a function called setupMovieInfo(data)
.
// fetching movie details
fetch(`${movie_detail_http}${movie_id}?` + new URLSearchParams({
api_key: api_key
}))
.then(res => res.json())
.then(data => {
setupMovieInfo(data);
})
Let's create setupMovieInfo
.
const setupMovieInfo = (data) => {
const movieName = document.querySelector('.movie-name');
const genres = document.querySelector('.genres');
const des = document.querySelector('.des');
const title = document.querySelector('title');
const backdrop = document.querySelector('.movie-info');
title.innerHTML = movieName.innerHTML = data.title;
genres.innerHTML = `${data.release_date.split('-')[0]} | `;
for(let i = 0; i < data.genres.length; i++){
genres.innerHTML += data.genres[i].name + formatString(i, data.genres.length);
}
if(data.adult == true){
genres.innerHTML += ' | +18';
}
if(data.backdrop_path == null){
data.backdrop_path = data.poster_path;
}
des.innerHTML = data.overview.substring(0, 200) + '...';
backdrop.style.backgroundImage = `url(${original_img_url}${data.backdrop_path})`;
}
Explanation
This function is very simple, at start it is selecting all elements like movie name, title tag, des, genres. And after selecting all the elements. We are setting the value using innerHTML
method. But for genres we have some conditions, like at first we are adding only released year by doing some formatting. And after that we are loopin through all the genres movie's data have and adding them to the genres with some formatting. And yes, you can see formatString
function let's create this.
const formatString = (currentIndex, maxIndex) => {
return (currentIndex == maxIndex - 1) ? '' : ', ';
}
After genres we are checking for backdrop_path
as we checked it before in homepage. And setting up the image as a background image.
Then as we don't get the cast info with the movies detail. We have to fetch it seperately.
//fetching cast info
fetch(`${movie_detail_http}${movie_id}/credits?` + new URLSearchParams({
api_key: api_key
}))
.then(res => res.json())
.then(data => {
const cast = document.querySelector('.starring');
for(let i = 0; i < 5; i++){
cast.innerHTML += data.cast[i].name + formatString(i, 5);
}
})
And I think this is very easy to understand. But if you have a doubt ask me in discussions.
Now if we see the output.
Output
Now let's fetch video clips.
/ fetching video clips
fetch(`${movie_detail_http}${movie_id}/videos?` + new URLSearchParams({
api_key: api_key
}))
.then(res => res.json())
.then(data => {
let trailerContainer = document.querySelector('.trailer-container');
let maxClips = (data.results.length > 4) ? 4 : data.results.length;
for(let i = 0; i < maxClips; i++){
trailerContainer.innerHTML += `
<iframe src="https://youtube.com/embed/${data.results[i].key}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
`;
}
})
Here, we are fetching videos detail related to movies. And after getting results we are checking a condition to set maxClips
because we want 4 clips at most. And after that we are looping maxClips
time. And creating an Iframe
this is the same structure that we have in our HTML file. Copy that from there to here. But notice it's src
attribute carefully.
Output
Now the last thing create recommendations.
// fetch recommendations
fetch(`${movie_detail_http}${movie_id}/recommendations?` + new URLSearchParams({
api_key: api_key
}))
.then(res => res.json())
.then(data => {
let container = document.querySelector('.recommendations-container');
for(let i = 0; i < 16; i++){
if(data.results[i].backdrop_path == null){
i++;
}
container.innerHTML += `
<div class="movie" onclick="location.href = '/${data.results[i].id}'">
<img src="${img_url}${data.results[i].backdrop_path}" alt="">
<p class="movie-title">${data.results[i].title}</p>
</div>
`;
}
})
And at the last step of the project. We are fetching similar movies like that from TMDB. And, after getting the data we are making only 16 cards. This is very similar to what we did for creating card in home.js
.
Output
We are done.
So, that's it. I hope you understood each and everything. If you have doubt or I missed some thing let me know in the comments.
Articles you may found Useful
- Infinte CSS loader
- Best CSS Effect
- Wave Button Hover Effect
- Youtube API - Youtube Clone
- Gradient Checkbox
I really appreciate if you can subscribe my youtube channel. I create awesome web contents. Subscribe
Thanks For reading.
Top comments (6)
This is very cool, I'm bookmarking it for later to get a full read-through!
Awesome 👍💯
I've seen several posts about people trying to create Youtube's UI clones, Facebook's IU clones, Twitter's UI clone.. what's the point?
These projects are just for practice purpose
HMTL ??
Oh! My bad I misspelled it. It is HTML.