Add a text or image overlay to a video element
Tags: react, video, daily, javascript
Published: 2020-12-03 18:44:48

We talk to a lot of developers building video calls at [Daily](https://dev.to/trydaily), and one thing they often want to do is overlay text (like a participant’s name) or small images (muted state indicators or logos) on top of a video element. This post walks through how to do that! 

![Person in Daily call points to their name at the top left corner of the video stream](https://dev-to-uploads.s3.amazonaws.com/i/wgyx7vhqeqteq6w4ajo9.png)
_It took me more time to take this screenshot with one hand than it took me to add my name._

First, we’ll cover the foundational CSS for positioning one element on top of another. Then, we’ll apply that CSS and build on top of Paul’s [React video chat app tutorial](https://dev.to/trydaily/build-a-video-chat-app-in-minutes-with-react-and-daily-js-481c). 

##Arrange and stack elements with CSS

We’ll set `position` and `z-index` properties to arrange our elements. 

[`position`](https://developer.mozilla.org/en-US/docs/Web/CSS/position) gives us control of how an element will sit in the overall layout of the page. When the property is not set, every block-level HTML element appears on a new line [0]. We don’t want that! We specifically want our name tag directly on top of and overlapping our video container. Where the name tag goes depends on the video's position.

To set up this dependent relationship, we set our video's `position` property to `relative`. Then, we can arrange any child elements, in our case our name tag, in relation to it by [setting their `position` property to `absolute`](https://css-tricks.com/absolute-positioning-inside-relative-positioning/).

To see this in action, experiment with removing `position:relative` from the `.parent-container` class in this codepen:

{% codepen https://codepen.io/kimberlee/pen/WNxWWZQ %}

Our boxes’ `top`, `bottom`, `right`, and `left` properties offset them relative to `.parent-container`. 

With the dependent relationship established, it's time to move on to stacking elements. To do that, we'll need the `z-index` property. Because we set `position` properties, we can make use of `z-index` to stack our elements. The higher the `z-index` number, the closer to the screen the element will be. Swap the `.red-box` and `.green-box` `z-index` values in the codepen to see what I mean. 

We now know how to arrange child elements in relation to their parents using `position`, and how to stack them with `z-index`. We’re ready to take those concepts over to our [React video chat app](https://dev.to/trydaily/build-a-video-chat-app-in-minutes-with-react-and-daily-js-481c), but first let’s look at how we can get participant names from the [Daily call object](https://docs.daily.co/docs/build-a-custom-video-chat-interface).

##Passing participant names as props in React

[The Daily call object](https://docs.daily.co/docs/build-a-custom-video-chat-interface) keeps track of our call state, meaning important information about the meeting. This includes details like other participants (e.g. their audio and video tracks and user_name) and the things they do on the call (e.g. muting their mic or leaving)[1]. The call object also provides methods for interacting with the meeting.

In our [demo app](https://github.com/daily-demos/call-object-react), we map the Daily call object state to a corresponding component state called `callItems` in `callState.js`. Each call item represents a participant, and contains their audio and video tracks, along with a boolean state indicator about whether or not their call is loading. To also track participant names, we'll add `participantName` to each call item.

```javascript
const initialCallState = {
 callItems: {
   local: {
     isLoading: true,
     audioTrack: null,
     videoTrack: null,
     participantName: '',
   },
 },
 clickAllowTimeoutFired: false,
 camOrMicError: null,
 fatalError: null,
};
```

We need to add `participantName` to our `getCallItems` function as well. This function loops over the call object to populate our `callItems`.

```javascript
function getCallItems(participants, prevCallItems) {
 let callItems = { ...initialCallState.callItems }; // Ensure we *always* have a local participant
 for (const [id, participant] of Object.entries(participants)) {
   // Here we assume that a participant will join with audio/video enabled.
   // This assumption lets us show a "loading" state before we receive audio/video tracks.
   // This may not be true for all apps, but the call object doesn't yet support distinguishing
   // between cases where audio/video are missing because they're still loading or muted.
   const hasLoaded = prevCallItems[id] && !prevCallItems[id].isLoading;
   const missingTracks = !(participant.audioTrack || participant.videoTrack);
   callItems[id] = {
     isLoading: !hasLoaded && missingTracks,
     audioTrack: participant.audioTrack,
     videoTrack: participant.videoTrack,
     participantName: participant.user_name ? participant.user_name : 'Guest',
   };
   if (participant.screenVideoTrack || participant.screenAudioTrack) {
     callItems[id + '-screen'] = {
       isLoading: false,
       videoTrack: participant.screenVideoTrack,
       audioTrack: participant.screenAudioTrack,
     };
   }
 }
 return callItems;
}
```

getCallItems gets called in Call.js [2]. It then passes the callItems as props via the getTiles function  to `<Tile>`, the component that displays each participant. We’ll add `participantName` to the list of props:

```javascript 
export default function Call() {
// Lots of other things happen here! See our demo for full code.
//
function getTiles() {
   let largeTiles = [];
   let smallTiles = [];
   Object.entries(callState.callItems).forEach(([id, callItem]) => {
     const isLarge =
       isScreenShare(id) ||
       (!isLocal(id) && !containsScreenShare(callState.callItems));
     const tile = (
       <Tile
         key={id}
         videoTrack={callItem.videoTrack}
         audioTrack={callItem.audioTrack}
         isLocalPerson={isLocal(id)}
         isLarge={isLarge}
         isLoading={callItem.isLoading}
         participantName={callItem.participantName}
         onClick={
           isLocal(id)
             ? null
             : () => {
                 sendHello(id);
               }
         }
       />
     );
     if (isLarge) {
       largeTiles.push(tile);
     } else {
       smallTiles.push(tile);
     }
   });
   return [largeTiles, smallTiles];
 }

 const [largeTiles, smallTiles] = getTiles();

return (
   <div className="call">
     <div className="large-tiles">
       {
         !message
           ? largeTiles
           : null /* Avoid showing large tiles to make room for the message */
       }
     </div>
     <div className="small-tiles">{smallTiles}</div>
     {message && (
       <CallMessage
         header={message.header}
         detail={message.detail}
         isError={message.isError}
       />
     )}
   </div>
 );
}
``` 

Now, in Tile.js, we display the name:

```javascript
export default function Tile(props) {
// More code
function getParticipantName() {
   return (
     props.participantName && (
       <div className="participant-name">{props.participantName}</div>
     )
   );
 }

 return (
   <div>
     <div className={getClassNames()} onClick={props.onClick}>
       <div className="background" />
       {getLoadingComponent()}
       {getVideoComponent()}
       {getAudioComponent()}
       {getParticipantName()}
     </div>
   </div>
 );
} 
``` 

And style it using familiar CSS in Tile.css, with our container tiles set to relative positioning and our video streams and name tags set to `absolute`:

```css
.tile.small {
 width: 200px;
 margin: 0 10px;
 position: relative;
}

.tile.large {
 position: relative;
 margin: 2px;
}

.tile video {
 width: 100%;
 position: absolute;
 top: 0px;
 z-index: 1;
}

.participant-name {
 padding: 5px 5px;
 position: absolute;
 background: #ffffff;
 font-family: 'Helvetica Neue';
 font-style: normal;
 font-weight: normal;
 font-size: 1rem;
 line-height: 13px;
 text-align: center;
 color: #4a4a4a;
 top: 0;
 left: 0;
 z-index: 10;
}
```

And there you have it!

If you have any questions or feedback about this post, please email me any time at kimberlee@daily.co. Or, if you’re looking to explore even more ways to customize Daily calls, [explore our docs](https://docs.daily.co/reference#introduction).

[0] This is not the case for [inline elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements). 

[1] A participant’s `user_name` can be set in a few different ways. It can be passed as a [property](https://docs.daily.co/reference#the-dailyiframe-class) to the DailyIframe, or [set with a meeting token](https://docs.daily.co/reference#meeting-tokens).

[2] More specifically, any time there’s a change to participants on the call, Call.js dispatches an action to a reducer that updates state via `getCallItems`.