Netflix UI clone

Building the Netflix UI clone with Vue 3 composition API and Tailwindcss


In this article, we will be learning how to use the new Vue 3 composition API.
We will explore using Axios to create an instance for API requests in our app. And how to use Tailwindcss to style our app.

Vue3 came with a few breaking changes, the biggest being the introduction of the composition API. We will explore some of the changes while we build the Netflix UI clone.

The Composition API, though not a replacement for the options API is a redesign of the Vue’s reactivity system. This new API helps us organise our code especially large codebase.
But to use the Composition API we need to know about the setup method and where to place it.

<script>
import {ref} from 'vue'
export default {
  props: ['Post'],
  setup(props) {
    const reactiveData = ref(5)
    console.log(props) // will return Post

    return {reactiveData}
  }
}
</script>

above the setup method is placed in our script and we use vue3 reactive ref to initialize any variable that is reactive in our component. The reactive variable is then returned in our return statement. Since there is nothing like this keyword in the setup method,we need to pass things like props in our setup method so it can be used.

You can familiarize yourself with the Composition API by reading getting starting with Vue 3 composition API article on CodeSource.

Live demo

What we will be needing

Breaking things down

we need to:

  • Setup our vue3 project
  • Get our movie API Key
  • Build header component
  • Hero section
  • create our request services
  • populating our app with movies from our API

Building the Netflix UI clone with Vue 3 composition API and Tailwindcss

You can start a new project using the vue cli but you have to install the cli first by running-

npm install -g @vue/cli
 OR
yarn global add @vue/cli

after the cli is installed you can create a new project by running-

vue create <project-name>

you will then be prompted to select some basic setup. In your setup, select vue router and vue3.
You can setup tailwindcss by following the official documentation here.
Then in the tailwind.config.js file, we paste the following config

module.exports = {
  purge: ["./src/**/*.html", "./src/**/*.vue"],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {
      inset: {
        "-50": "-50%"
      },
      width: {
        "200": "200%"
      },
      height: {
        "200": "200%"
      },
      minWidth: {
        "50": "50%"
      },
      minHeight: {
        "50": "50%"
      },
      zIndex: {
        "-10": "-10"
      },
      backgroundColor: {
        grey: "#181818"
      }
    }
  },
  variants: {
    extend: {
      zIndex: ["hover", "active"]
    }
  },
  plugins: []
};

Also install Axios by running

npm install axios
or
yarn add axios

Setup our movie API Key

0ur API key can be gotten from The Movie Database Api by registering an account with them, check your e-mail to verify your account, then go to the API page in your profile settings and you will see your API key.

At the root of your folder, create a file and name it env.js , then create and export an object containing the API key

export default {
  apiKey: "Put API key here",
}; 

Make sure you add the env.js file to the gitIgnore file so that you dont push your private API key to github.

Build header component

In components folder, create a file named Header.vue In the file using the setup method, we can create an object containing the links.

<script>
import { ref } from "vue";
export default {
  setup() {
    const navLinks = ref({
      Home: {
        name: "Home",
        href: "/"
      },
      TvShows: {
        name: "Shows",
        href: "/about"
      },
      Series: {
        name: "Series",
        href: "#"
      }
    });
    return { navLinks };
  }
};
</script>

above, we imported our reactive reference from vue, then in our setup method, we created a navLinks variable containing an object of all header links, and returned navLinks

In our template we can use the links declared in our setup method

<template>
  <div>
    <nav class="lg:block bg-gray-900 bg-opacity-70 z-10">
      <ul class="flex space-x-5 lg:space-x-8 items-center p-4">
        <li class="font-bold text-3xl text-red-600">VueFlix</li>
        <li v-for="link in navLinks" :key="link.name">
          <router-link
            :to="link.href"
            class="font-bold text-sm lg:text-base text-white hover:text-app-green-1 transition ease-in-out duration-300"
            >{{ link.name }}</router-link
          >
        </li>
      </ul>
    </nav>
  </div>
</template>

In our App.vue file, we import our header component and use it in our template.

<template>
  <div class="">
    <Header />
  </div>
  <router-view />
</template>
<script>
import Header from "@/components/Header.vue";
export default {
  components: {
    Header
  }
};
</script>

Hero section

In the views folder in our app, we can remove the about.vue page because we wont be using it. In the Home.vue page, we want something like this

To do this, we created a container, adding a background image and screen height to the container.

<template>
  <div class="home absolute inset-0 -z-10">
    <div
      style="
        background-image: url('https://wallpapercave.com/wp/wp6193606.jpg');
        background-position: center;
        background-repeat: no-repeat;
        background-size: cover;
        background-attachment: fixed;
      "
      class="hero text-left px-8 text-white h-screen relative"
    >
      <div class="absolute mt-72">
        <h1 class="text-4xl mb-3 lg:mb-5 lg:text-7xl xl:text-9xl font-semibold">
          SPIDER-MAN
        </h1>
        <p class="lg:text-2xl mb-4 lg:mb-6">
          With great power comes great responsibility
        </p>
        <div>
          <button
            class="bg-white rounded-lg font-bold text-gray-800 text-center px-4 py-3 transition duration-300 ease-in-out hover:bg-gray-300 mr-6"
          >
            Play
          </button>
          <button
            class="bg-gray-400 rounded-lg font-bold text-white text-center px-4 py-3 transition duration-300 ease-in-out hover:bg-white hover:text-gray-800 mr-6"
          >
            More Info
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

above we also added our hero text and two buttons.

Creating our request services

For us to be able to fetch the movies data, we need to create a service that can make request to our api endpoints and return the data.
In our app’s root directory, create a folder and name it Services. Then in the Services folder create a file and name it MovieService.js.

In this file, we will import axios and also our env.js file which contains our API key.

import axios from "axios";
import key from "../env";

then we create an axios instance where we set our baseUrl to request to, our headers and the timeout for each request.

const serviceInstance = axios.create({
  baseURL: "https://api.themoviedb.org/3",
  Headers: { Accept: "application/json" },
  timeout: 10000
});

After we’ve done this, we will export all our request. We are fetching three movie data for our app which are

  1. Popular Movies
  2. Trending Movies
  3. Top rated movies

Each of them with the same base Url. Our export from the MovieService.js file looks like this:

export default {
  getPopularMovies() {
    return serviceInstance.get(`/movie/popular?api_key=${key.apiKey}`);
  },
  getTrendingMovies() {
    return serviceInstance.get(`/trending/movie/week?api_key=${key.apiKey}`);
  },
  getTopMovies() {
    return serviceInstance.get(`/movie/top_rated?api_key=${key.apiKey}`);
  }
};

As we can see above, each method in our export is returning our axios instance and making a request using the baseUrl specified earlier joined with the full link of our request endpoint. Since we imported our env.js file as key we can access our api key in our request using Key.apiKey.

Populating our app with movies from our app

In our component folder, create a file named PopularMovies.vue. Then import our ref from vue and also our MovieService.

<script>
import { ref } from "vue";
import MovieService from "../../Services/MovieService.js";
</script>

using vue3 setup method we create two variables, one for storing the baseUrl of our image and the other for our popular movies data. Then we create an async function that fetches our data from the api and assigns it to the popular movies variable using async/await then returns our popular movies data and our base image url

<script>
import { ref } from "vue";
import MovieService from "../../Services/MovieService.js";
export default {
  setup() {
    const baseImgUrl = ref("https://image.tmdb.org/t/p");
    const moviesData = ref([]);
    async function loadData() {
      try {
        const moviedata = await MovieService.getPopularMovies();
        moviesData.value = moviedata.data.results;
        console.log(moviesData.value);
      } catch (err) {
        console.log(err);
      }
    }
    loadData();
    return { baseImgUrl, moviesData };
  }
};
</script>

Since we have gotten back our movie data, we can use it in our template.

<template>
  <div
    v-for="(movie, id) in moviesData"
    :key="id"
    class="flex flex-shrink-0 justify-center items-center w-1/2 max-w-sm mx-auto my-8"
  >
    <div
      :style="{
        backgroundImage: `url(${baseImgUrl}/w500${movie.backdrop_path})`
      }"
      class="bg-gray-300 h-64 w-full rounded-lg shadow-md bg-cover bg-center"
    ></div>
  </div>
</template>

Above we see that we use vue v-for to iterate through our data and display each movie. Another request is also made to our api from our template which is our background-image and we pass the url with the baseImgUrl and the size of the image w500 and the backdrop path from the movie data we got from the request made in our setup method.
Now our app looks something like this

Vue 3 composition API

But we also want to get top rated and Trending movies and display them below our most popular movies on our app.
So we create two files in our component folder and name them TopRated.vue and TrendingMovies.vue. Then we copy and paste the exact code we had in our
PopularMovies.vue file, but this time, we change the name of the variable we are storing the data gotten from the API and in the async function we request for the data with the name we gave it in the MovieServices.js file.

So our TopRated.vue file would look like this

<template>
  <div
    v-for="(movie, id) in topRatedData"
    :key="id"
    class="flex flex-shrink-0 justify-center items-center w-1/2 max-w-sm mx-auto my-8"
  >
    <div
      :style="{
        backgroundImage: `url(${baseImgUrl}/w500${movie.backdrop_path})`
      }"
      class="bg-gray-300 h-64 w-full rounded-lg shadow-md bg-cover bg-center"
    ></div>
  </div>
</template>
<script>
import { ref } from "vue";
import MovieService from "../../Services/MovieService.js";
export default {
  setup() {
    const baseImgUrl = ref("https://image.tmdb.org/t/p");
    const topRatedData = ref([]);
    // eslint-disable-next-line no-unused-vars
    async function loadData() {
      try {
        const moviedata = await MovieService.getTopMovies();
        topRatedData.value = moviedata.data.results;
        console.log(topRatedData.value);
      } catch (err) {
        console.log(err);
      }
    }
    loadData();
    return { baseImgUrl, topRatedData };
  }
};
</script>

and TrendingMovies.vue file looks like

<template>
  <div
    v-for="(movie, id) in trendingMoviesData"
    :key="id"
    class="flex flex-shrink-0 justify-center items-center w-1/2 max-w-sm mx-auto my-8"
  >
    <div
      :style="{
        backgroundImage: `url(${baseImgUrl}/w500${movie.backdrop_path})`
      }"
      class="bg-gray-300 h-64 w-full rounded-lg shadow-md bg-cover bg-center"
    ></div>
  </div>
</template>
<script>
import { ref } from "vue";
import MovieService from "../../Services/MovieService.js";
export default {
  setup() {
    const baseImgUrl = ref("https://image.tmdb.org/t/p");
    const trendingMoviesData = ref([]);
    // eslint-disable-next-line no-unused-vars
    async function loadData() {
      try {
        const moviedata = await MovieService.getTrendingMovies();
        trendingMoviesData.value = moviedata.data.results;
        console.log(trendingMoviesData.value);
      } catch (err) {
        console.log(err);
      }
    }
    loadData();
    return { baseImgUrl, trendingMoviesData };
  }
};
</script>

Then finally we import all components that display the movies in our Home.vue file

<script>
import PopularMovies from "@/components/PopularMovies.vue";
import TrendingMovies from "@/components/TrendingMovies.vue";
import TopRated from "@/components/TopRated.vue";
export default {
  name: "Home",
  components: {
    PopularMovies,
    TrendingMovies,
    TopRated
  }
};
</script>

And we add them to our template below our hero section

    <!-- movie display section -->
    <div class="w-full bg-grey">
      <h3 class="text-white text-2xl pt-6 ml-4 text-left">Most Popular</h3>
      <div class="flex space-x-4 overflow-y-hidden overflow-x-scroll">
        <PopularMovies />
      </div>
      <h3 class="text-white text-2xl pt-6 ml-4 text-left">Trending</h3>
      <div class="flex space-x-4 overflow-y-hidden overflow-x-scroll">
        <TrendingMovies />
      </div>
      <h3 class="text-white text-2xl pt-6 ml-4 text-left">Top Rated</h3>
      <div class="flex space-x-4 overflow-y-hidden overflow-x-scroll">
        <TopRated />
      </div>
    </div>

Conclusion

We have come to the end of this tutorial; I hope you learned a few things from the tutorial. In this article, we learned about vue 3 composition API, and data fetching with Axios. You can add more functionality, such as a movie details page, to our Netflix clone. you can access the Source code on Github.

Want to master Vue 3? we highly recommend Vue 3 master class – building Real-world apps by VueSchool.io, currently at 40% off Learn more here.


Share on social media

//