Launch Emoji Reaction Confetti in Your Twilio Video Chat with JavaScript

August 27, 2021
Written by
Mia Adjei
Twilion
Reviewed by

Launch Emoji Reaction Confetti in Your Twilio Video Chat with JavaScript

Have you ever used social media apps where you can react to someone's video by launching emoji reaction confetti? Sometimes you don't necessarily have a comment you want to share, but you do want to respond with a smile, a thumbs up, or a heart to show how you feel about what your friend is saying.

In this tutorial, you'll build a video application where participants can react to each other by launching emoji confetti. Participants will be able to see the emoji reactions that other people on the call are sharing, as well as share their own emoji reaction. This application will use the Twilio Video DataTrack API to share which emoji a participant has selected.

Let's get started!

Video chat between two yellow rubber ducks. Emoji confetti fly across part of the UI.

Prerequisites

  • A free Twilio account. (If you register here, you'll receive $10 in Twilio credit when you upgrade to a paid account!)
  • Node.js v14+ and npm installed on your machine.
  • ngrok

Create the project directory and install dependencies

From your terminal or command prompt, navigate to where you would like to set up your project. Create a new directory called data-track-emoji and change into that directory by running the following commands in your terminal:

mkdir data-track-emoji
cd data-track-emoji

Next, set up a new Node.js project with a default package.json file by running the following command:

npm init -y

Once you have your package.json file, you're ready to install the needed dependencies.


For this project, you will need the following packages:

Run the following command in your terminal to install these packages:

npm install express twilio dotenv node-dev

If you check your package.json file now, you'll notice that the packages above have been installed as dependencies.

Save your Twilio credentials safely as environment variables

Next, create a new file named .env at the root of your project and open it in your code editor. The .env file is where you will keep your Twilio account credentials. Add the following variables to your new file:

TWILIO_ACCOUNT_SID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_API_KEY_SID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_API_KEY_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

You’ll need to replace the placeholder text above with your actual Twilio credentials, which can be found in the Twilio Console. Log in to the Twilio Console and find your Account SID:

Twilio console, showing location of Account SID

Copy and paste the value for Account SID to replace the placeholder text for TWILIO_ACCOUNT_SID.

Then, navigate to the API Keys section of the console and generate a new API Key. Copy the API Key's values for SID and Secret to replace the placeholder text for TWILIO_API_KEY_SID and TWILIO_API_KEY_SECRET.

It’s important to keep these private credentials secure and out of version control. If you’re using GitHub, create a .gitignore file at the root of your project. Here you can list the files and directories that you want git to ignore from being tracked or committed. Open .gitignore in your code editor and add the .env file. While you're in here, add the node_modules directory as well:

.env
node_modules

Now that you have your Twilio credentials set up, it's time to create your Express server.

Create a new Express server

Create a new file called server.js at the root of your project. This is the place where you will write your server-side code.

Open up server.js in your code editor. Add the following lines of code to server.js, which will load the environment variables from your .env file, create a new Express application, and set the application to run on port 5000:

require('dotenv').config();
const express = require('express');
const app = express();
const port = 5000;

// Start the Express server
app.listen(port, () => {
  console.log(`Express server running on port ${port}`);
});

Now, open package.json in your code editor. Inside the scripts section, add a start script as shown below. You can replace the placeholder test script that was automatically generated by npm init -y earlier. Then, in the main section, change the entry file listed there to server.js. When you are finished, your package.json file should look like the code below:

{
  "name": "data-track-emoji",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node-dev server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "10.0.0",
    "express": "4.17.1",
    "node-dev": "7.0.0",
    "twilio": "3.67.0"
  }
}

Now, to run the start script, return to your terminal window and run the following command:

npm start

Once you have done this, you should see the following log statement in your terminal window, letting you know that the Express server is running:

Express server running on port 5000

Now that your server is running, it's time to lay out the client side of your application.

Build the application layout and serve static files

Create a new directory called public at the root of your project. This directory will hold your static files:

mkdir public

Inside the public directory, create 3 new files: app.js, index.html, and styles.css.

Open up public/index.html in your code editor and add the following code to the file:

<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible'>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <title>Emoji Confetti Video Chat</title>
    <link rel='stylesheet' type='text/css' href='styles.css'/>
    <script defer src='https://sdk.twilio.com/js/video/releases/2.16.0/twilio-video.min.js'></script>
    <script src='https://cdn.jsdelivr.net/npm/js-confetti@latest/dist/js-confetti.browser.js'></script>
    <script defer src='index.js' type='text/javascript'></script>
  </head>
  <body>
    <main>
        <section id='gallery'></section>

        <section id='reactions'>
          <canvas id='confetti'></canvas>
        </section>

        <section id='controls'>
          <form id='login'>
            <input type='text' id='identity' placeholder='Enter your name' required>
            <button id='button-join'>Join Room</button>
          </form>
          <form id='login'>
            <button id='button-leave' disabled>Leave Room</button>
          </form>

        </section>

        <section id='emoji'>
          <button id='heart' aria-label='heart emoji'>❤️</button>
          <button id='smile' aria-label='smile emoji'>😄</button>
          <button id='fire' aria-label='fire emoji'>🔥</button>
          <button id='party' aria-label='party emoji'>🎉</button>
        </section>
      </main>
  </body>
</html>

The HTML code above creates the layout for your video chat application. In the <head>, you have a link to the CSS styles, as well as three <script> tags: one for the Twilio Video library, one for the JS Confetti library, and one for the JavaScript code you'll write later in this tutorial.

The JS Confetti library lets you create confetti on an HTML canvas element. You can use regular confetti, emoji, or even letters of the alphabet. With this library, you'll create a <canvas> and can fill it with emoji at the press of a button! You can take a look at the JS Confetti demo here.

In the <body> of your HTML, there are four <section> elements:

  • gallery, which will include all participants' audio and video
  • reactions, which is where the emoji confetti will appear
  • controls, containing the input for a participant's name, as well as the buttons for joining and leaving the chat
  • emoji, with four buttons for selecting emoji reactions

Now that you have the HTML, open up public/styles.css and paste the following CSS styles into the file:

body {
  font-family: Arial, Helvetica, sans-serif;
  height: 100vh;
  padding: 0;
}

main {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  grid-template-rows: repeat(5, 1fr);
}

#gallery {
  grid-area: 1 / 1 / 5 / 5;
  background-color: #7cc881;
  display: flex;
  flex-flow: row wrap;
  flex-wrap: wrap;
  flex-direction: row-reverse;
  justify-content: center;
  align-items: baseline;
  align-content: start
}

#gallery video {
  flex: 0 0 auto;
  margin: 8px;
  width: 300px;
  border: 5px solid #fefc9e;
  border-radius: 5px;
}

.identity {
  font-size: 16px;
  text-align: center;
  color: #394469;
  padding-left: 1rem;
  z-index: 10;
}

#reactions {
  grid-area: 1 / 5 / 5 / 7;
  background-color: #737fac;
}

#reactions canvas {
  height: 700px;
  width: 100%;
}

#controls {
  grid-area: 5 / 1 / 6 / 5;
  background-color: #fea09e;
  padding: 4%;
}

form {
  display: inline-flex;
}

input {
  font-family: 'Roboto', sans-serif;
  font-size: 20px;
  text-indent: 16px;
  background-color: #f5c7c7;
  height: 35px;
  width: 280px;
  padding: 0.5em;
  margin-right: 5px;
  border: 1px transparent;
  border-radius: 6px;
}

button {
  border: none;
  background: #ffc6d0;
  color: #ffffff;
  font-size: 24px;
  padding: 0.5em;
  border-radius: 6px;
}

button:disabled,
input:disabled {
  opacity: .5
}

#emoji {
  grid-area: 5 / 5 / 6 / 7;
  background-color: #fefc9e;
  padding-left: 4%;
  padding-top: 4%;
}

#emoji button {
  width: 23%;
  margin-left: 1%;
  margin-top: 5%;
}

Now that you have some public files, you need to set up your server to serve these files when a user navigates to http://localhost:5000/ in their browser.

Return to server.js in your code editor. Add the following code to the file just below your constant definitions:

app.use(express.json());

// Serve static files from the public directory
app.use(express.static('public'));

app.get('/', (req, res) => {
  res.sendFile('public/index.html');
});

This code will add some Express middleware to your application, allowing it to parse JSON and to serve the static files you just created.

If you navigate to http://localhost:5000/ in your browser, you will see a colorful application that looks like the following:

Video application with green, blue, pink, and yellow sections. The yellow section contains 4 buttons with emoji on them.

Woohoo! Now that you have a layout set up, the next step is to add JavaScript on the client side to show the user a preview of their video feed.

Display a video preview for the local participant

Open public/index.js in your code editor. Add the following lines of code to set up the variables you will need for this part of the project:

const video = document.getElementById('video');
const gallery = document.getElementById('gallery');
const identityInput = document.getElementById('identity');

// buttons
const joinRoomButton = document.getElementById('button-join');
const leaveRoomButton = document.getElementById('button-leave');
const heartButton = document.getElementById('heart');
const smileButton = document.getElementById('smile');
const fireButton = document.getElementById('fire');
const partyButton = document.getElementById('party');

// confetti
const confettiCanvas = document.getElementById('confetti');
const jsConfetti = new JSConfetti({canvas: confettiCanvas});

// local data track
const localDataTrack = new Twilio.Video.LocalDataTrack();

const ROOM_NAME = 'emoji-party';
let videoRoom;

The first set of variables refer to the HTML sections and buttons in your app. You then have a variable that creates a new instance of the JSConfetti class, with your layout's canvas element passed in as the place for the confetti to appear.

After that, you have a variable that creates a new Twilio Video LocalDataTrack, which will be used later on to share participants' selected emoji. Following that are the variables for the room name — in this case, emoji-party — and a variable for the actual Twilio videoRoom that the participant will join.

To learn more about data tracks, take a look at the DataTrack API documentation here.

Next, paste the following code just below your list of variables. This is the function that will add a user's local video feed to the application:

const addLocalVideo = async () =>  {
  const videoTrack = await Twilio.Video.createLocalVideoTrack();
  const localVideoDiv = document.createElement('div');
  localVideoDiv.setAttribute('id', 'localParticipant');

  const trackElement = videoTrack.attach();
  localVideoDiv.appendChild(trackElement);

  gallery.appendChild(localVideoDiv);
};

In the addLocalVideo() function, you create a LocalVideoTrack, then attach it to a <div> and attach that <div> to the DOM. This is where the preview video will appear.

Just below where the addLocalVideo() function is declared, call the function:

// Show the participant a preview of their video
addLocalVideo();

Try this out now by refreshing the page in your browser window. You should see your video feed appear in the gallery!


You may see an alert dialog asking if localhost:5000 can use your camera. Once you allow this permission, you will be able to see your video feed.

Video application showing a local video feed with a yellow rubber duck looking into the camera.

Generate an Access Token for joining video rooms

Now that you can see your own video preview, it's time to get ready to join an actual video call. In order to do that, you will need a short-lived credential called an Access Token.

In this step, you will add code to your Express server to generate this Access Token.

Open server.js in your code editor and add the following imports to the list at the top of the file:

require('dotenv').config();
const express = require('express');
const app = express();
const port = 5000;
const AccessToken = require('twilio').jwt.AccessToken;
const VideoGrant = AccessToken.VideoGrant;

Then, add the following /token route just below your route for serving static files but before the app.listen() line:

app.post('/token', async (req, res) => {
  if (!req.body.identity || !req.body.room) {
    return res.status(400);
  }

  // Get the user's identity and the room name from the request
  const identity  = req.body.identity;
  const roomName  = req.body.room;

  try {
    // Create a video grant for this specific room
    const videoGrant = new VideoGrant({
      room: roomName,
    });

    // Create an access token
    const token = new AccessToken(
      process.env.TWILIO_ACCOUNT_SID,
      process.env.TWILIO_API_KEY_SID,
      process.env.TWILIO_API_KEY_SECRET,
    );

    // Add the video grant and the user's identity to the token
    token.addGrant(videoGrant);
    token.identity = identity;

    // Serialize the token to a JWT and return it to the client side
    return res.send({
      token: token.toJwt()
    });

  } catch (error) {
    return res.status(400).send({error});
  }
});

How will your application use this /token endpoint?

When a participant navigates to your application, enters their username in the input field, then clicks the Join Room button, their name and the name of the video room will be passed to this endpoint via a POST request.

The endpoint will then use your Twilio credentials and the Twilio Node Helper Library to create an access token specifically for this participant and this video room. Then, the endpoint will return the access token to the client side of your application. Your JavaScript code will then use this access token to connect to the video room.

Join the video room as a local participant

Now that you have the code for generating Access Tokens, it's time to write the code that will actually create the video room and let you join as a participant.

Return to public/index.js in your code editor. Just below your addLocalVideo() function definition and above its invocation, add the following new function called joinRoom():

const joinRoom = async (event) => {
  event.preventDefault();

  const identity = identityInput.value;

  try {
    const response = await fetch('/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        'identity': identity,
        'room': ROOM_NAME
      })
    });

    const data = await response.json();

    // Creates the audio and video tracks
    const localTracks = await Twilio.Video.createLocalTracks();

    // Include the data track
    const tracks = [...localTracks, localDataTrack];

    videoRoom = await Twilio.Video.connect(data.token, {
      name: ROOM_NAME,
      tracks: tracks
    });

    console.log(`You are now connected to Room ${videoRoom.name}`);

    const localParticipant = document.getElementById('localParticipant');
    const identityDiv = document.createElement('div');
    identityDiv.setAttribute('class', 'identity');
    identityDiv.innerHTML = identity;
    localParticipant.appendChild(identityDiv);

    videoRoom.participants.forEach(participantConnected);
    videoRoom.on('participantConnected', participantConnected);
    videoRoom.on('participantDisconnected', participantDisconnected);

    joinRoomButton.disabled = true;
    leaveRoomButton.disabled = false;
    identityInput.disabled = true;
  } catch (error) {
    console.log(error);
  }
}

This function will handle when a user clicks the Join Room button. It will get the user's identity from what they have entered in the <input> field, then make a POST request to the /token endpoint you added to your server in the previous section.

Once your application receives the access token from the server side, it creates local tracks for audio, video, and data, and connects to the video room using the token and these tracks. A new <div> is created with the participant's name (identity) and their media tracks. Then, the function sets up event listeners to listen for when other participants join the video call.

Now that the joinRoom() function is complete, it's time to create the function that will let users leave the video call. This function will disconnect a participant from the video room and remove all of the other participants' media from the UI.

Add the following leaveRoom() function just below the one for joinRoom():

const leaveRoom = (event) => {
  event.preventDefault();
  videoRoom.disconnect();
  console.log(`You are now disconnected from Room ${videoRoom.name}`);

  let removeParticipants = gallery.getElementsByClassName('participant');

  while (removeParticipants[0]) {
    gallery.removeChild(removeParticipants[0]);
  }

  localParticipant.removeChild(localParticipant.lastElementChild);

  joinRoomButton.disabled = false;
  leaveRoomButton.disabled = true;
  identityInput.disabled = false;
}

Now, at the bottom of public/index.js, add click event listeners to the joinRoomButton and leaveRoomButton that will call joinRoom() and leaveRoom() respectively when users click on these buttons:

// Show the participant a preview of their video
addLocalVideo();

// Event listeners
joinRoomButton.addEventListener('click', joinRoom);
leaveRoomButton.addEventListener('click', leaveRoom);

Now that you have a way for the local participant to join and leave the video room, it's time to add code to handle the other participants' connections.

Connect remote participants to the video room

You may have noticed, in the joinRoom() function you added before, that when a participantConnected event occurs in the video room, the function participantConnected() gets called. A similar thing occurs for the participantDisconnected event. It's time to add these two functions to the code in public/index.js.

Add the following participantConnected() function to the file, just below the disconnect() function:

const participantConnected = (participant) => {
  console.log(`${participant.identity} has joined the call.`);

  const participantDiv = document.createElement('div');
  participantDiv.setAttribute('id', participant.sid);
  participantDiv.setAttribute('class', 'participant');

  const tracksDiv = document.createElement('div');
  participantDiv.appendChild(tracksDiv);

  const identityDiv = document.createElement('div');
  identityDiv.setAttribute('class', 'identity');
  identityDiv.innerHTML = participant.identity;
  participantDiv.appendChild(identityDiv);

  gallery.appendChild(participantDiv);

  participant.tracks.forEach(publication => {
    if (publication.isSubscribed) {
      tracksDiv.appendChild(publication.track.attach());
    }
  });

  participant.on('trackSubscribed', track => {
    // Attach the video and audio tracks to the DOM
    if (track.kind === 'video' || track.kind === 'audio') {
      tracksDiv.appendChild(track.attach());
    }

    // Set up a listener for the data track
    if (track.kind === 'data') {
      // When a message is received via the data track, launch emoji confetti!
      track.on('message', emoji => {
        launchConfetti(emoji, null)
      });
    }
  });

  participant.on('trackUnsubscribed', track => {
    // Remove audio and video elements from the DOM
    if (track.kind === 'audio' || track.kind === 'video') {
      track.detach().forEach(element => element.remove());
    }
  });
};

The participantConnected() function creates a new <div> for each remote participant when they connect to the room, displays their username (identity), and attaches their video and audio tracks to the <div>.

The function also creates event listeners for when these remote participants publish or unpublish their tracks — if someone starts or stops sharing their media, your application can attach or detach these tracks from the UI as needed.

Additionally, this function also sets up a listener for when messages are received over the data track. In this application, when a participant's app receives data about which emoji someone has chosen, that emoji will be launched as confetti for everyone on the video call to see!

Now that you have code for participantConnected(), add the following participantDisconnected() function just below that:

const participantDisconnected = (participant) => {
  console.log(`${participant.identity} has left the call.`);
  document.getElementById(participant.sid).remove();
};

This function logs a message to the console about which participant has left the call and removes their <div> from the UI.

At this point, the video chat part of your application is fully functional. If you want to test it out, navigate to http://localhost:5000/ in your browser. Enter your name in the input field and click the Join Call button. You will be connected to the video room emoji-party.

Open a second browser tab to the same location, then join the video chat with a different name. You will see a <div> for each participant appear in the video gallery section of your application:

Video chat with two participants: "me" and "another me".

Now that your application is able to handle all participants' connection and disconnection events, it's time to make those emoji buttons actually launch confetti!

Launch emoji confetti at the press of a button

To customize and launch confetti, you'll use the addConfetti() function from JS Confetti to select which emoji to use, its size, how many pieces of confetti to create, and the confetti radius. You can use a switch statement to handle the selected emoji.

Add the following launchConfetti function just below your participantDisconnected function:

const launchConfetti = (selectedEmoji, participant = null) => {
  switch (selectedEmoji) {
    case 'heart emoji':
      jsConfetti.addConfetti({
        emojis: ['❤️'],
        emojiSize: 50,
        confettiNumber: 30,
        confettiRadius: 3
      });
      break;
    case 'smile emoji':
      jsConfetti.addConfetti({
        emojis: ['😄'],
        emojiSize: 50,
        confettiNumber: 30,
        confettiRadius: 3
      });
      break;
    case 'fire emoji':
      jsConfetti.addConfetti({
        emojis: ['🔥'],
        emojiSize: 50,
        confettiNumber: 30,
        confettiRadius: 3
      });
      break;
    default:
      jsConfetti.addConfetti({
        emojis: ['⭐', '👍', '✨', '🎉', '🌸'],
        emojiSize: 50,
        confettiNumber: 30,
        confettiRadius: 3
      });
      break;
  }

  // If the person who pressed the button to send the emoji is the local participant,
  // send the emoji to everyone else on the call
  if (participant && participant.id === 'localParticipant') {
    localDataTrack.send(selectedEmoji);
  }
}

In the function above, the switch statement evaluates the selected emoji, then calls addConfetti() for whichever one was chosen. If you are the person who clicked an emoji button, confetti of that emoji will appear on your canvas, and then the emoji will be sent to the data tracks of everyone else on the video call.

When the other participants' data tracks receive the message, their instance of the application will call launchConfetti() for the selected emoji, and the emoji confetti will appear in their UI as well!

At the bottom of public/index.js in the event listeners section, add event listeners to each of the emoji buttons so that launchConfetti will be called when a participant clicks the button:

// Event listeners
joinRoomButton.addEventListener('click', joinRoom);
leaveRoomButton.addEventListener('click', leaveRoom);

heartButton.addEventListener('click', (event) => { launchConfetti(event.target.ariaLabel, localParticipant) });
smileButton.addEventListener('click', (event) => { launchConfetti(event.target.ariaLabel, localParticipant) });
fireButton.addEventListener('click', (event) => { launchConfetti(event.target.ariaLabel, localParticipant) });
partyButton.addEventListener('click', (event) => { launchConfetti(event.target.ariaLabel, localParticipant) });

The emoji are selected by their aria-labels, which you added to the HTML earlier in this tutorial. Not only does this provide a useful way to reference these elements — it will also help make your application more accessible!

Run your application on ngrok

The last step before testing your application is to use ngrok to connect the Express server running locally on your machine to a temporary public URL. Once you have this URL, you can share the link with a friend and test the video chat together!

Open up a new terminal window pointing to the root of your project. To start a new ngrok tunnel, run the following command:

ngrok http 5000

Once ngrok is running, you will see text like the below in your terminal window:

ngrok by @inconshreveable                                                                                                             (Ctrl+C to quit)

Session Status                online
Account                       <YOUR_ACCOUNT_NAME>
Version                       2.3.40
Region                        <YOUR_REGION>
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://XXXXXXXXXXX.ngrok.io -> http://localhost:5000
Forwarding                    https://XXXXXXXXXXX.ngrok.io -> http://localhost:5000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

Copy one of the URLs next to Forwarding. You now have a temporary link to your application that you can share with a friend or colleague!

Chat over video and launch some emoji!

Paste the forwarding URL into the browser. Your application will load and you will see your video preview like before. If you click on the emoji buttons now, you will see a confetti launch! However, since you're not yet connected to a video room, no one else will be able to see this but you.

Enter your name in the input field and click the Join Call button. You will be connected to the video room emoji-party.

If you are testing this application on your own, open the same URL in a second browser window and join the call again with a different name. If you are testing this application with a friend, wait for your friend to join the video call.

Once everyone has joined the call, try clicking one of the emoji buttons. When one of you clicks a button, everyone will see the shower of emoji. Now you have a fun way to react to your friends' awesome stories, jokes, and comments during the video call!

Two video chat windows open side by side. The smile emoji confetti fills both applications&#x27; canvas elements.

What's next for your emoji-launching video chat?

If you would like to see the code for this project in its entirety, you can find the repository on GitHub here.

Now that you know how to add confetti to your video chat, you can customize the application even more. Maybe you would like to add your own favorite emoji or color scheme. You could even add some new features to the application, like muting and unmuting or screen sharing. Maybe you have your own exciting idea — the sky's the limit! ⭐️  I can't wait to see what you build!

Mia Adjei is a Software Developer on the Developer Voices team. They love to help developers build out new project ideas and discover aha moments. Mia can be reached at madjei [at] twilio.com.