Create a YouTube-Style Post-Video Completion CTA

Banner for a MediaJam post

Amarachi Iheanacho

YouTube has been and continues to be an essential piece in popular culture. With the addition of post-video completion call-to-action(CTA), users can dive in and find amazing video recommendations similar to what they have been watching.

This post will discuss implementing YouTube-style post-video completion call-to-action(CTA) using Cloudinary and Nuxt.js. At the end of this tutorial, we will learn to use callback functions to handle HTML5 video events.

What we will be building

Similar to YouTube on devices, when a video is done playing, the video player display portion is replaced by a grid of 4 cards, with each of these cards linking to a different video.

Youtube post-video completion CTA

Although the grid of cards covers the video display portion, the video player controls are still in view so that once the original video is played back, the card’s grid is hidden.


This project was completed in a Codesandbox. To get started quickly, fork the codesandbox or run the project.

GitHub Repository


To get the most out of this article it is important that we have the following:

  • A basic understanding of CSS, JavaScript and Vue.js
  • Node and its package manager, npm. Run the command node -v && npm -v to verify we have them installed, or install them from here. It is recommended that we have the latest version. Alternatively, we can use another package manager, Yarn.
  • Understanding Nuxtjs would help us follow through with this tutorial quicker, but it is not entirely required.
  • A Cloudinary account, if you don’t have one, you can create one here.

Setting up our Nuxtjs app

Nuxtjs is an open-source vue.js frontend development framework that allows us to create universal web applications without stress, render statically Vue.js applications without having a server, and enables functionalities like server-side rendering, etc. in our project.

To create a nuxtjs app, we go to our terminal and run the command below.

NOTE: If you are on windows and using Git Bash you might have issues with the arrows, so it is advisable that you use the Powershell terminal instead.

1npm init nuxt-app <project-name>
3 #or
5 npx create-nuxt-app <project-name>
7 #or
9 yarn create nuxt-app <project-name>

Running this command will trigger a set of question prompts. In the picture below this is the setup I used.

Nuxtjs setup prompts

Next we run these commands

1cd <project name>
3 npm run dev
5 #or
7 yarn dev

This would change the directory to the project we just created and run it on our browser, to see our app go to http://localhost:3000/

Nuxtjs setup page

Installing the Cloudinary dependency


Cloudinary is a cloud-based service that provides an end-to-end image and video management solution, including uploads, storage, and optimized delivery.

It also allows developers to embed video players in their app that handles video events effectively.

To enable our Nuxt.js app to use these Cloudinary features, we will add the video player assets in the head section of our nuxt.config.js file.

1// nuxt.config.js
3 export default {
4 head: {
5 ...
6 link: [
7 ...
8 { rel: 'stylesheet', href: '' }
9 ],
10 script: [
11 { src: '' },
12 { src: '' },
13 ],
14 },
16 };

Next up, we create a .env at the root of our project.

1touch .env

After creating our .env file, we go to our Dashboard on Cloudinary, in the Account Details section we can see our cloud name, copy it and paste it in our .env file.

1CLOUD_NAME = <Cloudinary-cloud-name>

Creating our Video Player

In our index.vue file, we embed the Cloudinary video player in our project, to do that, we use the HTML5 native video element.

2 <div>
3 <video
4 id= "video-player"
5 class="cld-video-player"
6 >
7 </video>
8 </div>
9 </template>

Now that we are done with that, in the mounted lifecycle hook of our index.vue file, we create a Cloudinary instance. Doing this in our mounted lifecycle hook allows the instance to be created once the app mounts.

1// pages/index.vue
3 <script>
5 export default {
6 data(){
7 return{
8 cld: null,
9 player: null,
10 video: "videoplayback_1_pr2hzi",
11 }
12 },
13 mounted(){
14 this.cld={
15 cloud_name: process.env.CLOUD_NAME,
16 secure: true
17 })
18 this.player = this.cld.videoPlayer(
19 'video-player', {
20 controls: true
21 }
22 );
23 this.player.source(;
24 }
26 }
29 </script>

In the data object, we define four variables to start with:

  • The cld variable holds the Cloudinary instance we will create.
  • The player variable has the new Cloudinary video player we will instantiate on mount.
  • The video variable holds the video's id we are looking to play. This video is stored on Cloudinary.
  • The controls variable controls the native player controls.

In our mounted lifecycle hook, we create a Cloudinary instance from the Cloudinary object by passing into it as an argument the cloud name we stored in our .env file and secure: true.

We then instantiate the Cloudinary video player by using the videoPlayer method and passing in two arguments:

  • The video player’s id or the video player element itself.
  • An object which sets the controls on the video to true.

Next, we add the video stored in our video variable as the source for the player.

In the style section of our index.vue file, we add a bit of styling to our video player, we centre the div containing our video player with the native CSS flex property, we then give our video player a width of 500px and a height of 500px.

1<style scoped>
2 div{
3 display: flex;
4 align-items: center;
5 justify-content: center;
6 }
7 .cld-video-player{
8 width: 500px;
9 height: 500px;
10 }
11 </style>

With that we have embedded a Cloudinary video player in our project.

Cloudinary Video Player

Creating the Overlay and Selectively rendering it.

Our overlay would contain four thumbnails that direct us to four different videos, to create this, we go into the template section of our index.vue file, using the HTML div element, we add a class of overlay.

We then create a card container that would hold four a tags each of which contains a div with a background image.

1<div class="overlay" >
2 <div class="cards">
3 <a href="">
4 <div :style="{ 'backgroundImage': 'url(' }"></div>
5 </a>
6 <a href="">
7 <div :style="{ 'backgroundImage': 'url(' }"></div>
8 </a>
9 <a href="">
10 <div :style="{ 'backgroundImage': 'url(' }"></div>
11 </a>
12 <a href="">
13 <div :style="{ 'backgroundImage': 'url(' }"></div>
14 </a>
15 </div>
16 </div>

But our overlay is incomplete without the styling. We add these styles in the style section of our index.vue file.

2 width: 500px;
3 height: 500px;
4 position: absolute;
5 background-color: rgba(0,0,0, 0.70);
7 }
8 .cards{
9 width: 80%;
10 height: 80%;
11 display: grid;
12 grid-template-columns: 1fr 1fr;
13 grid-template-rows: 1fr 1fr;
14 gap: 20px;
15 }
16 .cards a{
17 width: 100%;
18 height: 100%;
19 }
20 .cards div{
21 width: 100%;
22 height: 100%;
23 background-size: cover;
24 background-repeat: no-repeat;
25 }

We give our overlay a width and height of 500px. It also has an absolute position to sit on our video player.

The card’s div has a width and height of 80 per cent of its parent element. Using the native CSS grid for our layout, we specify that we want our card’s div to be divided into four equal parts (two per row and column).

Finally, for each div in the cards container, we want our background image to cover our entire div and not repeat itself.

Selectively rendering the overlay.

For our overlay to show up after the video has ended, we create an overlay variable on the data object in our index.vue file.

2 return{
3 ...
4 overlay: false
5 }
6 }

We then use this overlay variable to selectively render our overlay.

1<div class="overlay" v-if = 'overlay'>
2 <div class="cards">
3 <a href="">
4 <div :style="{ 'backgroundImage': 'url(' }"></div>
5 </a>
6 <a href="">
7 <div :style="{ 'backgroundImage': 'url(' }"></div>
8 </a>
9 <a href="">
10 <div :style="{ 'backgroundImage': 'url(' }"></div>
11 </a>
12 <a href="">
13 <div :style="{ 'backgroundImage': 'url(' }"></div>
14 </a>
15 </div>
16 </div>

Next, using Cloudinary's ended and play callback functions, we control the overlay variable.

1// Controls the overlay
3 this.player.on('ended', ()=> {
4 this.overlay= true;
5 })
6 this.player.on('play', ()=> {
7 this.overlay = false;
8 })

With that we have completed our project, we should have something like this.

Youtube-Style Post Video Completion CTA


This article discussed what Cloudinary is and how we can use it to handle player events to create a YouTube-style post-video completion CTA.


You may find the following resources useful.

Amarachi Iheanacho

Frontend Engineer and Technical Writer