Build Your Own UI
This guide explains how to use Cloudflare RealtimeKit SDKs to build fully custom real-time video UIs.
If you prefer to learn by seeing examples, please check out the respective example repositories.
If default meeting component is not enough, and you need more control over layout or behavior, use UI Kit components to build a custom interface. The UI Kit provides pre-built components that sit on top of the Core SDK, letting you mix and match pieces while saving time compared to building from scratch.
Building a custom UI requires managing participant audio, notifications, dialogs, component layout, and screen transitions yourself.
Similar to rtk-meeting, rtk-ui-provider, another ui-kit component that acts as a provider, also listens to states and syncs them with the UI Kit components.
Unlike rtk-meeting, rtk-ui-provider allows you to pass any child components to it, if any one of the child components is a RealtimeKit component starting with rtk-, rtk-ui-provider will coordinate with the RealtimeKit component to sync the states.
<!DOCTYPE html><html> <head> <script type="module"> import { defineCustomElements } from "https://cdn.jsdelivr.net/npm/@cloudflare/realtimekit-ui@latest/loader/index.es2017.js"; defineCustomElements(); </script> <script src="https://cdn.jsdelivr.net/npm/@cloudflare/realtimekit@latest/dist/browser.js"></script> </head>
<body style="margin: 0;"> <rtk-ui-provider id="rtk-ui-provider" style="display: flex; flex-direction: column; height: 100vh; margin: 0;" > <div id="meeting-container" style="display: flex; flex-direction: column; flex: 1; flex-grow: 1; flex-shrink: 1;" > Meeting will render here... </div> <rtk-participants-audio></rtk-participants-audio> <rtk-dialog-manager></rtk-dialog-manager> <rtk-notifications></rtk-notifications> </rtk-ui-provider> <script type="module"> async function initializeMeeting() { let currentState = "idle";
const meeting = await RealtimeKitClient.init({ authToken: "participant_auth_token", });
function renderSetupScreen() { document.querySelector("#meeting-container").innerHTML = ` <rtk-setup-screen></rtk-setup-screen> `; }
function renderWaitingScreen() { document.querySelector("#meeting-container").innerHTML = ` <rtk-waiting-screen></rtk-waiting-screen> `; }
function renderJoinedScreen() { document.querySelector("#meeting-container").innerHTML = ` <rtk-header style="display: flex; justify-content: space-between;"></rtk-header> <rtk-stage style="flex: 1; flex-grow: 1; flex-shrink: 1;"> <rtk-grid></rtk-grid> <rtk-sidebar style="position: fixed; top:0px;"></rtk-sidebar> </rtk-stage> <rtk-controlbar style="display: flex; justify-content: space-between;"></rtk-controlbar> `; }
function renderEndedScreen() { document.querySelector("#meeting-container").innerHTML = ` <rtk-ended-screen></rtk-ended-screen> `; }
// Listen for state updates from rtk-ui-provider document .querySelector("rtk-ui-provider") .addEventListener("rtkStatesUpdate", (event) => { // Store states to update your custom UI const states = event.detail;
if (states.meeting === "idle" && currentState !== "idle") { currentState = "idle"; document .querySelector("rtk-ui-provider") .querySelector("#meeting-container").innerHTML = "Meeting is loading..."; } else if (states.meeting === "setup" && currentState !== "setup") { currentState = "setup"; renderSetupScreen(); } else if ( states.meeting === "waiting" && currentState !== "waiting" ) { currentState = "waiting"; renderWaitingScreen(); } else if ( states.meeting === "joined" && currentState !== "joined" ) { currentState = "joined"; renderJoinedScreen(); } else if (states.meeting === "ended" && currentState !== "ended") { currentState = "ended"; renderEndedScreen(); }
const sidebarComponent = document .querySelector("rtk-ui-provider") .querySelector("#meeting-container") .querySelector("rtk-sidebar"); if (sidebarComponent) { if (states.activeSidebar) { sidebarComponent.style.display = "block"; } else { sidebarComponent.style.display = "none"; } } });
document.querySelector("rtk-ui-provider").showSetupScreen = true; document.querySelector("rtk-ui-provider").meeting = meeting; } initializeMeeting(); </script> </body></html>First level split of rtk-meeting using rtk-ui-provider has the following components:
rtk-header is the header component that shows the session name and the session controls.
rtk-stage is the container component that contains the grid and sidebar components.
rtk-grid is the grid component that shows the participants in the session.
rtk-sidebar is the sidebar component that shows the sidebar, in which chat, polls content shows up.
rtk-controlbar is the controlbar component that shows the controls, such as camera, microphone, etc.
rtk-notifications is the notifications component that shows the notifications for the session.
rtk-participants-audio is the audio component that helps you listen other participants in the session.
rtk-dialog-manager is the dialog-manager component that shows the all supported dialogs, such as settings, breakout rooms, etc.
You can split all of these components further. To see more such components, please refer to our components library.
We have our UI Kit open source on GitHub, you can find it here ↗.
Similar to RtkMeeting, RtkUIProvider, another ui-kit component that acts as a provider, also listens to states and syncs them with the UI Kit components.
Unlike RtkMeeting, RtkUIProvider allows you to pass any child components to it. If any one of the child components is a RealtimeKit component starting with Rtk, RtkUIProvider will coordinate with the RealtimeKit component to sync the states.
import { RealtimeKitProvider, useRealtimeKitClient,} from "@cloudflare/realtimekit-react";import { RtkUIProvider, RtkHeader, RtkStage, RtkGrid, RtkSidebar, RtkControlbar, RtkNotifications, RtkParticipantsAudio, RtkDialogManager, RtkSetupScreen, RtkWaitingScreen, RtkEndedScreen,} from "@cloudflare/realtimekit-react-ui";import { useEffect, useState } from "react";
function MeetingContainer() { const [meeting, initMeeting] = useRealtimeKitClient(); const [currentState, setCurrentState] = useState("idle"); const [showSidebar, setShowSidebar] = useState(false);
useEffect(() => { initMeeting({ authToken: "participant_auth_token", }); }, []);
const renderSetupScreen = () => { return <RtkSetupScreen />; };
const renderWaitingScreen = () => { return <RtkWaitingScreen />; };
const renderJoinedScreen = () => { return ( <> <RtkHeader style={{ display: "flex", justifyContent: "space-between" }} /> <RtkStage style={{ flex: 1, flexGrow: 1, flexShrink: 1 }}> <RtkGrid /> <RtkSidebar style={{ position: "fixed", top: "0px", display: showSidebar ? "block" : "none", }} /> </RtkStage> <RtkControlbar style={{ display: "flex", justifyContent: "space-between" }} /> </> ); };
const renderEndedScreen = () => { return <RtkEndedScreen />; };
// Listen for state updates from RtkUIProvider const handleStatesUpdate = (states) => { // Store states to update your custom UI if (states.meeting === "idle" && currentState !== "idle") { setCurrentState("idle"); } else if (states.meeting === "setup" && currentState !== "setup") { setCurrentState("setup"); } else if (states.meeting === "waiting" && currentState !== "waiting") { setCurrentState("waiting"); } else if (states.meeting === "joined" && currentState !== "joined") { setCurrentState("joined"); } else if (states.meeting === "ended" && currentState !== "ended") { setCurrentState("ended"); }
// Update sidebar visibility based on state if (states.activeSidebar !== undefined) { setShowSidebar(states.activeSidebar); } };
return ( <RealtimeKitProvider value={meeting}> <RtkUIProvider meeting={meeting} showSetupScreen={true} onRtkStatesUpdate={handleStatesUpdate} style={{ display: "flex", flexDirection: "column", height: "100vh", margin: 0, }} > <div id="meeting-container" style={{ display: "flex", flexDirection: "column", flex: 1, flexGrow: 1, flexShrink: 1, }} > {currentState === "idle" && <div>Meeting is loading...</div>} {currentState === "setup" && renderSetupScreen()} {currentState === "waiting" && renderWaitingScreen()} {currentState === "joined" && renderJoinedScreen()} {currentState === "ended" && renderEndedScreen()} </div> <RtkParticipantsAudio /> <RtkDialogManager /> <RtkNotifications /> </RtkUIProvider> </RealtimeKitProvider> );}
function App() { return <MeetingContainer />;}First level split of RtkMeeting using RtkUIProvider has the following components:
RtkHeader is the header component that shows the session name and the session controls.
RtkStage is the container component that contains the grid and sidebar components.
RtkGrid is the grid component that shows the participants in the session.
RtkSidebar is the sidebar component that shows the sidebar, in which chat, polls content shows up.
RtkControlbar is the controlbar component that shows the controls, such as camera, microphone, etc.
RtkNotifications is the notifications component that shows the notifications for the session.
RtkParticipantsAudio is the audio component that helps you listen other participants in the session.
RtkDialogManager is the dialog-manager component that shows the dialogs for the session.
You can split all of these components further. To see more such components, please refer to our components library.
We have our UI Kit open source on GitHub, you can find it here ↗.
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark
-