We design our SDKs for web and mobile to give developers as much flexibility as possible when building video calls. For example, you can drop a ready-to-use video chat into an existing page with a few lines of code using our prebuilt UI, or build a completely custom call from scratch with the Daily call object.
One thing Daily developers 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!
First, we’ll go over the CSS to position one element on top of another. Then, we’ll add those CSS properties to Paul’s Daily React app.
Arrange and stack elements with CSS
We’ll set position
and z-index
properties to arrange our elements.
position
gives us control of how an element will sit in the overall layout of the page. When the property isn't 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
.
To see this in action, experiment with removing position:relative
from the .parent-container
class in this codepen:
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 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 demo, but first let’s look at how we can get participant names from the Daily call object.
Passing participant names as props in React
The Daily call object 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, 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.
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
.
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:
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:
// Imports and such
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:
.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.
[0] This is not the case for inline elements.
[1] A participant’s user_name can be set in a few different ways. It can be passed as a property to the DailyIframe, or set with a meeting token.
[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
.