---
title: Build Your Own UI
description: Build a custom meeting interface video UI using RealtimeKit SDK components and Core SDK.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/realtime/llms.txt  
> Use this file to discover all available pages before exploring further.

[Skip to content](#%5Ftop) 

# Build Your Own UI

This guide explains how to use RealtimeKit UI Kit components to build a custom meeting interface instead of the default full-screen meeting view.

## Prerequisites

This page builds upon the [Initialize SDK](https://developers.cloudflare.com/realtime/realtimekit/core/), [Render Default Meeting UI](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/), and [UI Kit States](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/state-management/) guides. First refer to these pages to understand the core concepts.

The code examples on this page assume you have already imported the necessary packages and initialized the SDK.

## Build a custom UI with UI Kit

If the default meeting component does not provide enough control over layout or behavior, use [UI Kit components](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/) to build a custom interface. The UI Kit provides pre-built components on top of the Core SDK. You can mix and match pieces while saving time compared to building from scratch.

A custom UI requires you to manage participant audio, notifications, dialogs, component layout, and screen transitions.

WebMobile

ReactWeb ComponentsAngular

Similar to `rtk-meeting`, `rtk-ui-provider`, another UI Kit component that acts as a provider, also listens to [states](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/state-management/) 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 child component is a RealtimeKit component starting with `rtk-`, `rtk-ui-provider` will coordinate with it to sync the states.

Similar to `RtkMeeting`, `RtkUiProvider`, another UI Kit component that acts as a provider, also listens to [states](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/state-management/) and syncs them with the UI Kit components.

Unlike `RtkMeeting`, `RtkUiProvider` allows you to pass any child components to it. If any child component is a RealtimeKit component starting with `Rtk`, `RtkUiProvider` will coordinate with it to sync the states.

Similar to `rtk-meeting`, `rtk-ui-provider`, another UI Kit component that acts as a provider, also listens to [states](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/state-management/) 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 child component is a RealtimeKit component starting with `rtk-`, `rtk-ui-provider` will coordinate with it to sync the states.

The iOS SDK uses a **delegation pattern** for custom UIs. Instead of wrapping child views in a provider, you conform to `RtkUIFlowCoordinatorDelegate` and return your own view controllers for each screen in the meeting flow.

There are two levels of customization:

* **Replace entire screens** — Conform to `RtkUIFlowCoordinatorDelegate` and return your own `UIViewController` for the setup screen, group call screen, or webinar screen. Return `nil` to use the default screen.
* **Replace sub-components** — Conform to `MeetingViewControllerDataSource` and swap the topbar, middle view (participant grid), or bottom control bar within the default `MeetingViewController`.

Swift

```

public protocol RtkUIFlowCoordinatorDelegate {

    func showSetUpScreen(completion: () -> Void) -> SetupViewControllerDataSource?

    func showGroupCallMeetingScreen(meeting: RealtimeKitClient, completion: @escaping () -> Void) -> UIViewController?

    func showWebinarMeetingScreen(meeting: RealtimeKitClient, completion: @escaping () -> Void) -> UIViewController?

}


```

When the user joins successfully, the SDK calls `showGroupCallMeetingScreen(meeting:completion:)` or `showWebinarMeetingScreen(meeting:completion:)`. Use the `RealtimeKitClient` parameter to bind individual UI Kit components (such as `RtkMeetingHeaderView`, `RtkMeetingControlBar`, or `RtkParticipantTileView`) to the meeting.

The Android SDK does not use a provider/wrapper pattern. Instead, each individual View component has an `activate(meeting)` method that binds it to the meeting.

The pattern is:

1. Build a `RealtimeKitUI` instance with `RealtimeKitUIBuilder.build()`.
2. Access the `RealtimeKitClient` via `rtkUI.meeting`.
3. Place individual UI Kit views in your own XML layout or create them in code.
4. Call `activate(meeting)` on each component to bind it to the meeting.

Meeting-level components (grid, header, control bar) take a `RealtimeKitClient`:

Kotlin

```

gridView.activate(meeting)

headerView.activate(meeting)

controlBarView.activate(meeting)


```

Participant-level components (tile, avatar, name tag) take an `RtkMeetingParticipant`:

Kotlin

```

tileView.activate(meeting.localUser)

tileView.activate(remoteParticipant)


```

The Flutter SDK provides `RtkProvider`, a wrapper widget that sets up dependency injection and theming for individual UI Kit widgets.

Wrap your custom widget tree in `RtkProvider` and use individual exported widgets such as `RtkSelfAudioToggleButton`, `RtkSelfVideoToggleButton`, `RtkParticipantTile`, `RtkJoinButton`, and `RtkLeaveButton`.

Dart

```

RtkProvider(

  meeting: meeting,

  uiKitInfo: RealtimeKitUIInfo(meetingInfo),

  child: YourCustomMeetingWidget(),

)


```

Each widget that needs the meeting client accepts it as a required `meeting` parameter. Participant-level widgets such as `RtkParticipantTile` accept an `RtkMeetingParticipant` directly.

Similar to `RtkMeeting`, `RtkUIProvider` is a wrapper component that provides the design system and UI state context to all child components.

Unlike `RtkMeeting`, `RtkUIProvider` allows you to pass any child components to it. All RealtimeKit UI Kit components read from the `RtkUIContext` for design tokens and state.

Use `useContext(RtkUIContext)` to read `storeStates.meeting` and determine which screen to render. When building without `RtkMeeting`, you must listen to SDK events and call `setStates` to drive state transitions yourself.

## Example code

```

<!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>


```

Note

It is advised to always use `rtk-ui-provider` to render a custom UI. Without `rtk-ui-provider`, you will have to set props, such as `meeting`, to all the UI Kit components manually.

`rtk-ui-provider` helps you go from

```

<rtk-header />

<!-- Later do document.querySelector('rtk-header').meeting = meeting; -->


```

to just

```

<rtk-header />


```

It is recommended to use either `rtk-meeting` or `rtk-ui-provider` to render a meeting. Using both will result in unexpected behavior. For custom UIs, it is recommended to use `rtk-ui-provider` always.

First level split of `rtk-meeting` using `rtk-ui-provider` has the following components:

[rtk-header](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-header) is the header component that shows the session name and the session controls.  
[rtk-stage](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-stage) is the container component that contains the grid and sidebar components.  
[rtk-grid](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-grid) is the grid component that shows the participants in the session.  
[rtk-sidebar](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-sidebar) is the sidebar component that shows the sidebar, in which chat, polls content shows up.  
[rtk-controlbar](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-controlbar) is the controlbar component that shows the controls, such as camera, microphone, etc.  
[rtk-notifications](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-notifications) is the notifications component that shows the notifications for the session.  
[rtk-participants-audio](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-participants-audio) is the audio component that helps you listen other participants in the session.  
[rtk-dialog-manager](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#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](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/).

We have our UI Kit open source on GitHub, you can find it [here ↗](https://github.com/cloudflare/realtimekit-ui).

Note

Please note that you will need to manage the CSS for aligning these components, yourself. This was previously handled entirely by `rtk-meeting`. All these components can be styled using CSS.

Note

You must include `rtk-notifications`, `rtk-participants-audio`, and `rtk-dialog-manager`. If you leave them out, features like settings toggles and notifications won’t work, and you won’t hear other participants in the session.

```

import {

  RealtimeKitProvider,

  useRealtimeKitClient,

} from "@cloudflare/realtimekit-react";

import {

  RtkUiProvider,

  RtkHeader,

  RtkStage,

  RtkGrid,

  RtkSidebar,

  RtkControlbar,

  RtkNotifications,

  RtkParticipantsAudio,

  RtkDialogManager,

  RtkSetupScreen,

  RtkWaitingScreen,

  RtkEndedScreen,

  States,

} 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 = (event: { detail: States }) => {

    const meetingState = event.detail.meeting;

    const states = event.detail;


    // Store states to update your custom UI

    if (meetingState === "idle" && currentState !== "idle") {

      setCurrentState("idle");

    } else if (meetingState === "setup" && currentState !== "setup") {

      setCurrentState("setup");

    } else if (meetingState === "waiting" && currentState !== "waiting") {

      setCurrentState("waiting");

    } else if (meetingState === "joined" && currentState !== "joined") {

      setCurrentState("joined");

    } else if (meetingState === "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 />;

}


```

Note

It is advised to always use `RtkUiProvider` to render a custom UI. Without `RtkUiProvider`, you will have to set props, such as `meeting`, to all the UI Kit components manually.

`RtkUiProvider` helps you go from

```

<RtkHeader meeting={meeting} />


```

to just

```

<RtkHeader />


```

It is recommended to use either `RtkMeeting` or `RtkUiProvider` to render a meeting. Using both will result in unexpected behavior. For custom UIs, it is recommended to use `RtkUiProvider` always.

First level split of `RtkMeeting` using `RtkUiProvider` has the following components:

[RtkHeader](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtkheader) is the header component that shows the session name and the session controls.  
[RtkStage](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtkstage) is the container component that contains the grid and sidebar components.  
[RtkGrid](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtkgrid) is the grid component that shows the participants in the session.  
[RtkSidebar](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtksidebar) is the sidebar component that shows the sidebar, in which chat, polls content shows up.  
[RtkControlbar](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtkcontrolbar) is the controlbar component that shows the controls, such as camera, microphone, etc.  
[RtkNotifications](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtknotifications) is the notifications component that shows the notifications for the session.  
[RtkParticipantsAudio](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtkparticipantsaudio) is the audio component that helps you listen other participants in the session.  
[RtkDialogManager](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#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](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/).

We have our UI Kit open source on GitHub, you can find it [here ↗](https://github.com/cloudflare/realtimekit-ui).

Note

Please note that you will need to manage the CSS for aligning these components, yourself. This was previously handled entirely by `RtkMeeting`. All these components can be styled using CSS.

Note

You must include `RtkNotifications`, `RtkParticipantsAudio`, and `RtkDialogManager`. If you leave them out, features like settings toggles and notifications won't work, and you won't hear other participants in the session.

In your app.module.ts, import the RealtimeKitComponentsModule along with all the custom modules you have built using the component library (example shown below).

app.module.ts

```

import { NgModule } from "@angular/core";

import { BrowserModule } from "@angular/platform-browser";

import { FormsModule } from "@angular/forms";


import { AppComponent } from "./app.component";

import { CustomRtkMeetingComponent } from "./components/custom-rtk-meeting.component";

import { SetupScreenComponent } from "./components/setup-screen.component";

import { InMeetingComponent } from "./components/in-meeting.component";

import { MeetingHeaderComponent } from "./components/meeting-header.component";

import { MeetingControlBarComponent } from "./components/meeting-control-bar.component";

import { MeetingSidebarComponent } from "./components/meeting-sidebar.component";

import { MediaPreviewModalComponent } from "./components/media-preview-modal.component";

import { AudioPreviewComponent } from "./components/audio-preview.component";

import { VideoPreviewComponent } from "./components/video-preview.component";


import { RealtimeKitComponentsModule } from "@cloudflare/realtimekit-angular-ui";


@NgModule({

  declarations: [

    AppComponent,

    CustomRtkMeetingComponent,

    SetupScreenComponent,

    InMeetingComponent,

    MeetingHeaderComponent,

    MeetingControlBarComponent,

    MeetingSidebarComponent,

    MediaPreviewModalComponent,

    AudioPreviewComponent,

    VideoPreviewComponent,

  ],

  imports: [BrowserModule, FormsModule, RealtimeKitComponentsModule],

  providers: [],

  bootstrap: [AppComponent],

})

export class AppModule {}


```

Initialize the meeting in your app.component.ts

app.component.ts

```

import { Component, OnInit, Inject } from "@angular/core";

import { DOCUMENT } from "@angular/common";

import { StatesService } from "./services/states.service";

import RealtimeKitClient from "@cloudflare/realtimekit";


@Component({

  selector: "app-root",

  templateUrl: "./app.component.html",

  styleUrls: ["./app.component.css"],

})

export class AppComponent implements OnInit {

  meeting: any = null;


  constructor(

    private statesService: StatesService,

    @Inject(DOCUMENT) private document: Document,

  ) {}


  async ngOnInit() {

    await this.initializeMeeting();

  }


  private async initializeMeeting() {

    const searchParams = new URLSearchParams(

      this.document.defaultView?.location.search,

    );

    const authToken = searchParams.get("authToken");


    if (!authToken) {

      alert(

        "An authToken wasn't passed, please pass an authToken in the URL query to join a meeting.",

      );

      return;

    }


    if (!this.meeting) {

      try {

        // Initialize RealtimeKit client

        const meeting = await RealtimeKitClient.init({

          authToken,

        });


        this.meeting = meeting;


        // Expose meeting object to window for debugging

        Object.assign(this.document.defaultView as any, {

          meeting: this.meeting,

        });

      } catch (error) {

        console.error("Failed to initialize meeting:", error);

      }

    }

  }


  onRtkStatesUpdate(event: any) {

    this.statesService.setStates(event.detail);

  }

}


```

app.component.html

```

<rtk-ui-provider

  [meeting]="meeting"

  (rtkStatesUpdate)="onRtkStatesUpdate($event)"

  [showSetupScreen]="true"

  style="height: 100%; width: 100%; display: block;"

>

  <app-custom-rtk-meeting></app-custom-rtk-meeting>

  <rtk-dialog-manager></rtk-dialog-manager>

</rtk-ui-provider>


```

This is an example of what a custom element built using UI Kit components looks like:

components/custom-rtk-component.ts

```

import { Component, OnInit, OnDestroy } from "@angular/core";

import { Subject, takeUntil } from "rxjs";

import { StatesService, CustomStatesService } from "../services/states.service";

import { States } from "@cloudflare/realtimekit-ui";

import { CustomStates } from "../types";


@Component({

  selector: "app-custom-rtk-meeting",

  template: `

    <rtk-idle-screen *ngIf="states.meeting === 'idle'"></rtk-idle-screen>

    <app-setup-screen *ngIf="states.meeting === 'setup'"></app-setup-screen>

    <rtk-waiting-screen

      *ngIf="states.meeting === 'waiting'"

    ></rtk-waiting-screen>

    <rtk-ended-screen *ngIf="states.meeting === 'ended'"></rtk-ended-screen>

    <app-in-meeting

      *ngIf="states.meeting === 'joined' || !states.meeting"

    ></app-in-meeting>

  `,

})

export class CustomRtkMeetingComponent implements OnInit, OnDestroy {

  states: States = { meeting: "idle" } as States;

  customStates: CustomStates = {};

  private destroy$ = new Subject<void>();


  constructor(

    private statesService: StatesService,

    private customStatesService: CustomStatesService,

  ) {}


  ngOnInit() {

    this.statesService.states$

      .pipe(takeUntil(this.destroy$))

      .subscribe((states) => {

        this.states = states;

        console.log(states, this.customStates);

      });


    this.customStatesService.customStates$

      .pipe(takeUntil(this.destroy$))

      .subscribe((customStates) => {

        this.customStates = customStates;

        console.log(this.states, customStates);

      });

  }


  ngOnDestroy() {

    this.destroy$.next();

    this.destroy$.complete();

  }

}


```

Note

It is advised to always use `rtk-ui-provider` to render a custom UI. Without `rtk-ui-provider`, you will have to set props, such as `meeting`, to all the UI Kit components manually.

`rtk-ui-provider` helps you go from

```

<rtk-header />

<!-- Later do document.querySelector('rtk-header').meeting = meeting; -->


```

to just

```

<rtk-header />


```

It is recommended to use either `rtk-meeting` or `rtk-ui-provider` to render a meeting. Using both will result in unexpected behavior. For custom UIs, it is recommended to use `rtk-ui-provider` always.

First level split of `rtk-meeting` using `rtk-ui-provider` has the following components:

[rtk-header](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-header) is the header component that shows the session name and the session controls.  
[rtk-stage](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-stage) is the container component that contains the grid and sidebar components.  
[rtk-grid](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-grid) is the grid component that shows the participants in the session.  
[rtk-sidebar](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-sidebar) is the sidebar component that shows the sidebar, in which chat, polls content shows up.  
[rtk-controlbar](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-controlbar) is the controlbar component that shows the controls, such as camera, microphone, etc.  
[rtk-notifications](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-notifications) is the notifications component that shows the notifications for the session.  
[rtk-participants-audio](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#rtk-participants-audio) is the audio component that helps you listen other participants in the session.  
[rtk-dialog-manager](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/#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](https://developers.cloudflare.com/realtime/realtimekit/ui-kit/component-library/).

We have our UI Kit open source on GitHub, you can find it [here ↗](https://github.com/cloudflare/realtimekit-ui).

Note

Please note that you will need to manage the CSS for aligning these components, yourself. This was previously handled entirely by `rtk-meeting`. All these components can be styled using CSS.

Warning

You must include `rtk-notifications`, `rtk-participants-audio`, and `rtk-dialog-manager`. If you leave them out, features like settings toggles and notifications will not work, and you will not hear other participants in the session.

The following example replaces the group call meeting screen with a custom view controller that uses individual UI Kit components:

Swift

```

import UIKit

import RealtimeKit

import RealtimeKitUI


class CustomFlowDelegate: RtkUIFlowCoordinatorDelegate {

    // Return nil to use the default setup screen

    func showSetUpScreen(completion: () -> Void) -> SetupViewControllerDataSource? {

        return nil

    }


    // Return a custom view controller for group calls

    func showGroupCallMeetingScreen(

        meeting: RealtimeKitClient,

        completion: @escaping () -> Void

    ) -> UIViewController? {

        return CustomMeetingViewController(meeting: meeting, completion: completion)

    }


    // Return nil to use the default webinar screen

    func showWebinarMeetingScreen(

        meeting: RealtimeKitClient,

        completion: @escaping () -> Void

    ) -> UIViewController? {

        return nil

    }

}


class CustomMeetingViewController: UIViewController {

    private let meeting: RealtimeKitClient

    private let completion: () -> Void

    private var selfListener: RtkEventSelfListener?


    init(meeting: RealtimeKitClient, completion: @escaping () -> Void) {

        self.meeting = meeting

        self.completion = completion

        super.init(nibName: nil, bundle: nil)

    }


    required init?(coder: NSCoder) { fatalError("init(coder:) not supported") }


    override func viewDidLoad() {

        super.viewDidLoad()

        view.backgroundColor = .black


        // Header

        let header = RtkMeetingHeaderView(meeting: meeting)

        header.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(header)


        // Control bar

        let controlBar = RtkMeetingControlBar(

            meeting: meeting,

            delegate: nil,

            presentingViewController: self

        )

        controlBar.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(controlBar)


        NSLayoutConstraint.activate([

            header.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),

            header.leadingAnchor.constraint(equalTo: view.leadingAnchor),

            header.trailingAnchor.constraint(equalTo: view.trailingAnchor),

            header.heightAnchor.constraint(equalToConstant: 50),

            controlBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),

            controlBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),

            controlBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),

            controlBar.heightAnchor.constraint(equalToConstant: 80),

        ])


        // Listen for meeting events

        selfListener = RtkEventSelfListener(rtkClient: meeting)

        selfListener?.observeSelfRemoved { [weak self] _ in

            self?.dismiss(animated: true, completion: self?.completion)

        }

        selfListener?.observeSelfMeetingEndForAll { [weak self] _ in

            self?.dismiss(animated: true, completion: self?.completion)

        }

    }

}


```

Present the meeting with your custom flow delegate:

Swift

```

let delegate = CustomFlowDelegate()

let rtkUI = RealtimeKitUI(

    meetingInfo: RtkMeetingInfo(

        authToken: "<auth-token>",

        enableAudio: true,

        enableVideo: true

    ),

    flowDelegate: delegate

)


let controller = rtkUI.startMeeting {

    self.dismiss(animated: true)

}

controller.modalPresentationStyle = .fullScreen

present(controller, animated: true)


```

Individual components available for custom meeting screens:

| Component                     | Description                                                                  |
| ----------------------------- | ---------------------------------------------------------------------------- |
| RtkMeetingHeaderView          | Header with meeting title, participant count, clock, and recording indicator |
| RtkMeetingControlBar          | Control bar with microphone, camera, and end meeting buttons                 |
| RtkParticipantTileView        | Single participant video tile with avatar fallback and name tag              |
| RtkVideoView                  | Raw video rendering view                                                     |
| RtkAvatarView                 | Participant avatar with initials                                             |
| RtkNameTag                    | Participant name with microphone status icon                                 |
| RtkAudioButtonControlBar      | Standalone microphone toggle button                                          |
| RtkVideoButtonControlBar      | Standalone camera toggle button                                              |
| RtkEndMeetingControlBarButton | End/leave meeting button                                                     |
| RtkEventSelfListener          | Observer for audio/video state, removal, reconnection, and permissions       |

The following example creates a custom meeting screen using individual UI Kit views in an XML layout with the `activate(meeting)` pattern:

activity\_custom\_meeting.xml

```

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

    xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="vertical"

    android:background="#000000">


    <com.cloudflare.realtimekit.ui.view.headers.RtkMeetingHeaderView

        android:id="@+id/headerView"

        android:layout_width="match_parent"

        android:layout_height="wrap_content" />


    <com.cloudflare.realtimekit.ui.view.grid.RtkGridView

        android:id="@+id/gridView"

        android:layout_width="match_parent"

        android:layout_height="0dp"

        android:layout_weight="1" />


    <com.cloudflare.realtimekit.ui.view.controlbars.RtkMeetingControlBarView

        android:id="@+id/controlBarView"

        android:layout_width="match_parent"

        android:layout_height="wrap_content" />


</LinearLayout>


```

CustomMeetingActivity.kt

```

import android.os.Bundle

import androidx.appcompat.app.AppCompatActivity

import com.cloudflare.realtimekit.RealtimeKitClient

import com.cloudflare.realtimekit.RtkMeetingRoomEventListener

import com.cloudflare.realtimekit.models.RtkMeetingInfo

import com.cloudflare.realtimekit.ui.RealtimeKitUIBuilder

import com.cloudflare.realtimekit.ui.RealtimeKitUIInfo

import com.cloudflare.realtimekit.ui.view.controlbars.RtkMeetingControlBarView

import com.cloudflare.realtimekit.ui.view.grid.RtkGridView

import com.cloudflare.realtimekit.ui.view.headers.RtkMeetingHeaderView


class CustomMeetingActivity : AppCompatActivity() {

    private lateinit var headerView: RtkMeetingHeaderView

    private lateinit var gridView: RtkGridView

    private lateinit var controlBarView: RtkMeetingControlBarView


    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_custom_meeting)


        headerView = findViewById(R.id.headerView)

        gridView = findViewById(R.id.gridView)

        controlBarView = findViewById(R.id.controlBarView)


        val meetingInfo = RtkMeetingInfo(authToken = "<auth-token>")

        val uiKitInfo = RealtimeKitUIInfo(

            activity = this,

            rtkMeetingInfo = meetingInfo,

        )

        val rtkUI = RealtimeKitUIBuilder.build(uiKitInfo)

        val meeting: RealtimeKitClient = rtkUI.meeting


        // Listen for meeting lifecycle events

        meeting.addMeetingRoomEventListener(object : RtkMeetingRoomEventListener {

            override fun onMeetingRoomJoinCompleted(meeting: RealtimeKitClient) {

                runOnUiThread {

                    // Activate all components once the meeting is joined

                    headerView.activate(meeting)

                    gridView.activate(meeting)

                    controlBarView.activate(meeting)

                }

            }


            override fun onMeetingEnded() {

                finish()

            }


            override fun onMeetingRoomLeaveCompleted() {

                finish()

            }

        })

    }

}


```

Individual components available for custom layouts:

| Component                | Activate with         | Description                                                          |
| ------------------------ | --------------------- | -------------------------------------------------------------------- |
| RtkGridView              | RealtimeKitClient     | Paginated participant video grid                                     |
| RtkMeetingHeaderView     | RealtimeKitClient     | Header with title, participant count, clock, and recording indicator |
| RtkMeetingControlBarView | RealtimeKitClient     | Control bar with mic, camera, more, and leave buttons                |
| RtkParticipantTileView   | RtkMeetingParticipant | Single participant video tile with avatar and name tag               |
| RtkMicToggleButton       | RealtimeKitClient     | Standalone microphone toggle                                         |
| RtkCameraToggleButton    | RealtimeKitClient     | Standalone camera toggle                                             |
| RtkLeaveButton           | RealtimeKitClient     | Leave/end meeting button                                             |
| RtkAvatarView            | RtkMeetingParticipant | Participant avatar                                                   |
| RtkNameTagView           | RtkMeetingParticipant | Name tag with audio indicator                                        |
| RtkJoinButton            | RealtimeKitClient     | Join meeting button for setup screens                                |

Note

You must manage layout and sizing for these components yourself. Use standard Android layout techniques (XML, ConstraintLayout, LinearLayout) to position components.

The following example wraps a custom widget tree in `RtkProvider` and uses individual UI Kit widgets:

custom\_meeting.dart

```

import 'package:flutter/material.dart';

import 'package:realtimekit_ui/realtimekit_ui.dart';


class CustomMeetingPage extends StatefulWidget {

  final String authToken;

  const CustomMeetingPage({required this.authToken, super.key});


  @override

  State<CustomMeetingPage> createState() => _CustomMeetingPageState();

}


class _CustomMeetingPageState extends State<CustomMeetingPage>

    implements RtkMeetingRoomEventListener {

  late final RealtimekitClient meeting;

  late final RealtimeKitUIInfo uiKitInfo;

  String meetingState = 'idle';


  @override

  void initState() {

    super.initState();

    meeting = RealtimekitClient();

    uiKitInfo = RealtimeKitUIInfo(

      RtkMeetingInfo(authToken: widget.authToken),

    );

    meeting.addMeetingRoomEventListener(this);

    meeting.init(

      RtkMeetingInfo(authToken: widget.authToken),

      onSuccess: () => setState(() => meetingState = 'setup'),

      onError: (error) => setState(() => meetingState = 'error'),

    );

  }


  @override

  void onMeetingRoomJoinCompleted() {

    setState(() => meetingState = 'joined');

  }


  @override

  void onMeetingEnded() {

    setState(() => meetingState = 'ended');

  }


  @override

  void onMeetingRoomLeaveCompleted() {

    Navigator.of(context).pop();

  }


  // Required interface methods — no-op for this example

  @override

  void onMeetingInitCompleted() {}

  @override

  void onMeetingInitFailed(Exception exception) {}

  @override

  void onMeetingInitStarted() {}

  @override

  void onMeetingRoomJoinStarted() {}

  @override

  void onMeetingRoomJoinFailed(Exception exception) {}

  @override

  void onMeetingRoomLeaveStarted() {}

  @override

  void onActiveTabUpdate(ActiveTab? activeTab) {}

  @override

  void onSocketConnectionUpdate(SocketConnectionState state) {}


  @override

  Widget build(BuildContext context) {

    return RtkProvider(

      meeting: meeting,

      uiKitInfo: uiKitInfo,

      child: Scaffold(

        backgroundColor: Colors.black,

        body: _buildForState(),

      ),

    );

  }


  Widget _buildForState() {

    switch (meetingState) {

      case 'setup':

        return Column(

          children: [

            Expanded(

              child: RtkParticipantTile(meeting.localUser, height: 300, width: 200),

            ),

            Row(

              mainAxisAlignment: MainAxisAlignment.center,

              children: [

                RtkSelfAudioToggleButton(meeting: meeting),

                const SizedBox(width: 16),

                RtkSelfVideoToggleButton(meeting: meeting),

                const SizedBox(width: 16),

                RtkJoinButton(meeting: meeting, onMeetingJoined: () {}),

              ],

            ),

          ],

        );

      case 'joined':

        return Column(

          children: [

            RtkMeetingTitle(meeting: meeting),

            Expanded(

              child: GridView.builder(

                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(

                  crossAxisCount: 2,

                  childAspectRatio: 3 / 4,

                ),

                itemCount: meeting.participants.active.length + 1,

                itemBuilder: (context, index) {

                  final participant = index == 0

                      ? meeting.localUser

                      : meeting.participants.active[index - 1];

                  return RtkParticipantTile(participant);

                },

              ),

            ),

            Row(

              mainAxisAlignment: MainAxisAlignment.spaceEvenly,

              children: [

                RtkSelfAudioToggleButton(meeting: meeting),

                RtkSelfVideoToggleButton(meeting: meeting),

                RtkLeaveButton(meeting: meeting),

              ],

            ),

          ],

        );

      case 'ended':

        return const Center(child: Text('Meeting ended', style: TextStyle(color: Colors.white)));

      default:

        return const Center(child: CircularProgressIndicator());

    }

  }


  @override

  void dispose() {

    meeting.removeMeetingRoomEventListener(this);

    super.dispose();

  }

}


```

Individual widgets available for custom UIs:

| Widget                      | Required parameter    | Description                                                    |
| --------------------------- | --------------------- | -------------------------------------------------------------- |
| RtkSelfAudioToggleButton    | RealtimekitClient     | Microphone toggle with permission handling                     |
| RtkSelfVideoToggleButton    | RealtimekitClient     | Camera toggle with permission handling                         |
| RtkJoinButton               | RealtimekitClient     | Join meeting button with loading state                         |
| RtkLeaveButton              | RealtimekitClient     | Leave meeting button with confirmation dialog                  |
| RtkParticipantTile          | RtkMeetingParticipant | Video tile with avatar fallback, name tag, and audio indicator |
| RtkMeetingTitle             | RealtimekitClient     | Meeting title text                                             |
| RtkNameTag                  | RtkMeetingParticipant | Participant name label                                         |
| RtkAudioIndicatorIconWidget | RtkMeetingParticipant | Microphone status icon                                         |

Note

The Flutter SDK exports a focused set of composable widgets. For a complete meeting experience, combine these widgets with the Core SDK event listeners (`RtkMeetingRoomEventListener`, `RtkParticipantsEventListener`, `RtkSelfEventListener`) to manage meeting lifecycle and participant state.

The following example uses `RtkUIProvider` and individual components with state-based screen rendering, similar to the web React pattern:

Warning

When building without `RtkMeeting`, you must manage state transitions yourself. Listen for `roomJoined`, `roomLeft`, and socket events on the meeting object, and call `setStates` to update `storeStates.meeting` between `idle`, `setup`, `waiting`, `joined`, and `ended`.

CustomMeeting.tsx

```

import React, { useContext, useEffect } from "react";

import { View, Text } from "react-native";

import {

  useRealtimeKitClient,

  RealtimeKitProvider,

} from "@cloudflare/realtimekit-react-native";

import {

  RtkUIProvider,

  RtkUIContext,

  RtkHeader,

  RtkGrid,

  RtkControlbar,

  RtkSidebar,

  RtkDialogManager,

  RtkSetupScreen,

  RtkWaitingScreen,

  RtkEndedScreen,

} from "@cloudflare/realtimekit-react-native-ui";


function MeetingScreens({ meeting }) {

  const { storeStates, setStates } = useContext(RtkUIContext);

  const currentState = storeStates.meeting;


  useEffect(() => {

    // Listen for meeting events and drive state transitions

    const onRoomJoined = () => {

      setStates({ ...storeStates, meeting: "joined" });

    };

    const onRoomLeft = ({ state }) => {

      if (state === "ended" || state === "left") {

        setStates({ ...storeStates, meeting: "ended" });

      }

    };

    meeting.self.addListener("roomJoined", onRoomJoined);

    meeting.self.addListener("roomLeft", onRoomLeft);

    return () => {

      meeting.self.removeListener("roomJoined", onRoomJoined);

      meeting.self.removeListener("roomLeft", onRoomLeft);

    };

  }, [meeting]);


  if (currentState === "setup") {

    return <RtkSetupScreen meeting={meeting} />;

  }


  if (currentState === "waiting") {

    return <RtkWaitingScreen />;

  }


  if (currentState === "joined") {

    return (

      <View style={{ flex: 1 }}>

        <RtkHeader meeting={meeting} />

        <View style={{ flex: 1 }}>

          <RtkGrid meeting={meeting} />

          {storeStates.activeSidebar && <RtkSidebar meeting={meeting} />}

        </View>

        <RtkControlbar meeting={meeting} />

        <RtkDialogManager meeting={meeting} />

      </View>

    );

  }


  if (currentState === "ended") {

    return <RtkEndedScreen meeting={meeting} />;

  }


  return <Text>Loading...</Text>;

}


function CustomMeeting({ authToken }) {

  const [meet, initMeeting] = useRealtimeKitClient();

  const { setStates, storeStates } = useContext(RtkUIContext);


  useEffect(() => {

    initMeeting({

      authToken,

      defaults: { audio: true, video: true },

    });

  }, [authToken]);


  useEffect(() => {

    if (meet) {

      setStates({ ...storeStates, meeting: "setup" });

    }

  }, [meet]);


  if (!meet) return <Text>Initializing...</Text>;


  return (

    <RealtimeKitProvider value={meet}>

      <MeetingScreens meeting={meet} />

    </RealtimeKitProvider>

  );

}


export default function App() {

  return (

    <RtkUIProvider>

      <CustomMeeting authToken="<auth-token>" />

    </RtkUIProvider>

  );

}


```

First level split of `RtkMeeting` has the following components:

| Component          | Required prop        | Description                                                                  |
| ------------------ | -------------------- | ---------------------------------------------------------------------------- |
| RtkHeader          | meeting              | Header with meeting title, participant count, clock, and recording indicator |
| RtkGrid            | meeting              | Paginated participant video grid                                             |
| RtkControlbar      | meeting              | Control bar with mic, camera, screenshare, and more toggles                  |
| RtkSidebar         | meeting              | Sidebar with chat, polls, participants, and plugins panels                   |
| RtkDialogManager   | meeting              | Manages all dialogs (settings, leave confirmation, join stage)               |
| RtkSetupScreen     | meeting              | Pre-join screen with video preview and device selection                      |
| RtkWaitingScreen   | —                    | Waiting room screen                                                          |
| RtkEndedScreen     | meeting              | Meeting ended screen                                                         |
| RtkMicToggle       | meeting              | Standalone microphone toggle button                                          |
| RtkCameraToggle    | meeting              | Standalone camera toggle button                                              |
| RtkLeaveButton     | —                    | Leave/end meeting button                                                     |
| RtkParticipantTile | meeting, participant | Single participant video tile                                                |
| RtkNotifications   | meeting              | Toast notifications for join/leave/chat events                               |

Note

Wrap your component tree in `RtkUIProvider`. All UI Kit components read design tokens and state from `RtkUIContext`. Without `RtkUIProvider`, components will not render correctly.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/realtime/","name":"Realtime"}},{"@type":"ListItem","position":3,"item":{"@id":"/realtime/realtimekit/","name":"RealtimeKit"}},{"@type":"ListItem","position":4,"item":{"@id":"/realtime/realtimekit/ui-kit/","name":"Build using UI Kit"}},{"@type":"ListItem","position":5,"item":{"@id":"/realtime/realtimekit/ui-kit/build-your-own-ui/","name":"Build Your Own UI"}}]}
```
