Add Custom Header

Last updated Oct 19, 2026 Prerequisite

This guide builds on top of the Build Your Own UI. It is recommended to read that guide first. Portions of the code will not be repeated here for brevity.

For a complete list of available UI Kit components, refer to the Component Library.

In this guide, we will learn how to add a custom header for your RealtimeKit meeting experience.

WebMobile

ReactWeb ComponentsAngular

RealtimeKit UI provides the RtkHeader component for a default header.

If you need additional controls, replace RtkHeader with individual UI Kit components and custom elements.

Import the required components:

import {
    RtkLogo,
    RtkRecordingIndicator,
    RtkLivestreamIndicator,
    RtkMeetingTitle,
    RtkGridPagination,
    RtkParticipantCount,
    RtkViewerCount,
    RtkClock,
} from "@cloudflare/realtimekit-ui";

In your RtkUIProvider from Build Your Own UI, replace:

<RtkHeader />

with:

<div
    style={{
        display: "flex",
        justifyContent: "space-between",
        backgroundColor: "black",
        color: "white",
    }}
>
    <div
        id="header-left"
        style={{ display: "flex", alignItems: "center", height: "48px" }}
    >
        <RtkLogo />
        <RtkRecordingIndicator />
        <RtkLivestreamIndicator />
    </div>
    <div
        id="header-center"
        style={{ display: "flex", alignItems: "center", height: "48px" }}
    >
        <RtkMeetingTitle />
    </div>
    <div
        id="header-right"
        style={{ display: "flex", alignItems: "center", height: "48px" }}
    >
        <RtkGridPagination />
        <RtkParticipantCount />
        <RtkViewerCount />
        <RtkClock />
        <button onClick={handleReportBugClick}>Report Bug</button>
    </div>
</div>

Define the click handler:

const handleReportBugClick = () => {
    console.log("Report Bug Clicked");
};

For a complete example, refer to the custom UI example ↗︎ and the custom header component ↗︎.

RealtimeKit UI provides the rtk-header component for a default header.

If you need additional controls, replace rtk-header with individual UI Kit components and custom elements. In the renderJoinedScreen function from Build Your Own UI, replace:

<rtk-header style="display: flex; justify-content: space-between;"></rtk-header>

with:

<div
    style="display: flex; justify-content: space-between; align-items: center; height: 48px; padding: 0 12px; background-color: black; color: white;"
>
    <div style="display: flex; align-items: center; gap: 8px;">
        <rtk-logo></rtk-logo>
        <rtk-recording-indicator></rtk-recording-indicator>
        <rtk-livestream-indicator></rtk-livestream-indicator>
    </div>

<div style="display: flex; align-items: center;">
        <rtk-meeting-title></rtk-meeting-title>
    </div>

<div style="display: flex; align-items: center; gap: 8px;">
        <rtk-grid-pagination></rtk-grid-pagination>
        <rtk-participant-count></rtk-participant-count>
        <rtk-viewer-count></rtk-viewer-count>
        <rtk-clock></rtk-clock>
        <button id="report-bug-button" type="button">Report Bug</button>
    </div>
</div>

Register the click handler after rendering:

document.querySelector("#report-bug-button").addEventListener("click", () => {
    console.log("Report Bug Clicked");
});

For a complete example, refer to the custom UI example ↗︎ and the custom header component ↗︎.

RealtimeKit UI provides the rtk-header component for a default header.

If you need additional controls, replace rtk-header with individual UI Kit components and custom elements. Create a custom header component that uses the RealtimeKit angular components directly.

Create Custom Header Component

custom-header.component.tstypescript

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

@Component({
    selector: "app-custom-header",
    template: `
        <div class="custom-header">
            <div class="header-left">
                <rtk-logo></rtk-logo>
                <rtk-recording-indicator></rtk-recording-indicator>
                <rtk-livestream-indicator></rtk-livestream-indicator>
            </div>

<div class="header-center">
                <rtk-meeting-title></rtk-meeting-title>
            </div>

<div class="header-right">
                <rtk-grid-pagination></rtk-grid-pagination>
                <rtk-participant-count></rtk-participant-count>
                <rtk-viewer-count></rtk-viewer-count>
                <rtk-clock></rtk-clock>
                <button
                    type="button"
                    class="report-bug-button"
                    (click)="onReportBugClick()"
                >
                    Report Bug
                </button>
            </div>
        </div>
    `,
    styles: [
        `
            .custom-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                height: 48px;
                padding: 0 12px;
                background-color: black;
                color: white;
            }

.header-left,
            .header-right {
                display: flex;
                align-items: center;
                gap: 8px;
            }

.header-center {
                display: flex;
                align-items: center;
            }

.report-bug-button {
                background: none;
                border: 1px solid white;
                color: white;
                padding: 4px 8px;
                border-radius: 4px;
                cursor: pointer;
                font-size: 12px;
            }

.report-bug-button:hover {
                background-color: rgba(255, 255, 255, 0.1);
            }
        `,
    ],
})
export class CustomHeaderComponent implements AfterViewInit {
    ngAfterViewInit() {
        console.log("Custom header initialized");
    }

onReportBugClick() {
        console.log("Report Bug Clicked");
        // Add your custom logic here
        // For example: open a modal, navigate to a form, etc.
    }
}

Use in Your Meeting Component

In your main meeting component template, replace:

<rtk-header style="display: flex; justify-content: space-between;"></rtk-header>

with:

<app-custom-header></app-custom-header>

Complete Meeting Component Example

meeting.component.tstypescript

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

@Component({
    selector: "app-meeting",
    template: `
        <rtk-meeting #meetingComponent id="meeting-component">
            <!-- Custom header replaces rtk-header -->
            <app-custom-header></app-custom-header>

<!-- Other meeting UI components -->
            <rtk-grid></rtk-grid>
            <rtk-sidebar></rtk-sidebar>
        </rtk-meeting>
    `,
})
export class MeetingComponent implements OnInit, OnDestroy {
    @ViewChild("meetingComponent", { static: true }) meetingElement!: ElementRef;

meeting: any;
    private authToken = "<participant_auth_token>";

async ngOnInit() {
        const RealtimeKitClient =
            await import("https://cdn.jsdelivr.net/npm/@cloudflare/realtimekit@latest/dist/index.es.js");

this.meeting = await RealtimeKitClient.default.init({
            authToken: this.authToken,
        });

const meetingComponent = this.meetingElement.nativeElement;
        meetingComponent.showSetupScreen = true;
        meetingComponent.meeting = this.meeting;
    }

ngOnDestroy() {
        // Cleanup logic
    }
}

Module Configuration

Don't forget to declare your custom header component in your Angular module:

app.module.tstypescript

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";

import { AppComponent } from "./app.component";
import { MeetingComponent } from "./meeting.component";
import { CustomHeaderComponent } from "./custom-header.component";

@NgModule({
    declarations: [AppComponent, MeetingComponent, CustomHeaderComponent],
    imports: [BrowserModule],
    providers: [],
    bootstrap: [AppComponent],
    schemas: [CUSTOM_ELEMENTS_SCHEMA], // Required for RTK web components
})
export class AppModule {}

This approach gives you full control over the header layout while maintaining Angular's component architecture and event handling patterns.

RealtimeKit UI provides the RtkMeetingHeaderView component for a default header.

If you need additional controls, replace RtkMeetingHeaderView with individual UI Kit components and custom elements.

Import the required components:

import RealtimeKitUI

Create custom header view

Create a custom header view that uses the RealtimeKit iOS components directly:

CustomHeaderView.swiftswift

import UIKit
import RealtimeKit
import RealtimeKitUI

class CustomHeaderView: UIView {
    private let meeting: RealtimeKitClient

private lazy var titleLabel: RtkMeetingTitleLabel = {
        return RtkMeetingTitleLabel(meeting: meeting)
    }()

private lazy var participantCountView: RtkParticipantCountView = {
        return RtkParticipantCountView(meeting: meeting)
    }()

private lazy var clockView: RtkClockView = {
        return RtkClockView(meeting: meeting)
    }()

private lazy var recordingView: RtkRecordingView = {
        return RtkRecordingView(meeting: meeting)
    }()

private let reportBugButton = UIButton(type: .system)

init(meeting: RealtimeKitClient) {
        self.meeting = meeting
        super.init(frame: .zero)
        setupUI()
    }

required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

private func setupUI() {
        backgroundColor = .black

// Configure report bug button
        reportBugButton.setTitle("Report Bug", for: .normal)
        reportBugButton.setTitleColor(.white, for: .normal)
        reportBugButton.layer.borderColor = UIColor.white.cgColor
        reportBugButton.layer.borderWidth = 1
        reportBugButton.layer.cornerRadius = 4
        reportBugButton.contentEdgeInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
        reportBugButton.addTarget(self, action: #selector(handleReportBugTap), for: .touchUpInside)

// Create stack views for layout
        let leftStack = UIStackView(arrangedSubviews: [recordingView])
        leftStack.axis = .horizontal
        leftStack.spacing = 8
        leftStack.alignment = .center

let centerStack = UIStackView(arrangedSubviews: [titleLabel])
        centerStack.axis = .horizontal
        centerStack.alignment = .center

let rightStack = UIStackView(arrangedSubviews: [
            participantCountView,
            clockView,
            reportBugButton
        ])
        rightStack.axis = .horizontal
        rightStack.spacing = 8
        rightStack.alignment = .center

let mainStack = UIStackView(arrangedSubviews: [leftStack, centerStack, rightStack])
        mainStack.axis = .horizontal
        mainStack.distribution = .equalSpacing
        mainStack.alignment = .center
        mainStack.translatesAutoresizingMaskIntoConstraints = false

addSubview(mainStack)

NSLayoutConstraint.activate([
            mainStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
            mainStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
            mainStack.topAnchor.constraint(equalTo: topAnchor),
            mainStack.bottomAnchor.constraint(equalTo: bottomAnchor),
            heightAnchor.constraint(equalToConstant: 48)
        ])
    }

@objc private func handleReportBugTap() {
        print("Report Bug Tapped")
        // Add your custom logic here
    }
}

Use in your meeting view controller

In your MeetingViewController, replace the default header with your custom header:

MeetingViewController.swiftswift

import UIKit
import RealtimeKit
import RealtimeKitUI

class MeetingViewController: UIViewController {
    private var meeting: RealtimeKitClient?
    private var customHeader: CustomHeaderView?

override func viewDidLoad() {
        super.viewDidLoad()
        setupMeeting()
    }

private func setupMeeting() {
        Task {
            do {
                let client = try await RealtimeKitClient.init(authToken: "<PARTICIPANT_AUTH_TOKEN>")
                self.meeting = client
                await MainActor.run {
                    setupCustomHeader(meeting: client)
                }
            } catch {
                print("Failed to initialize meeting: \(error)")
            }
        }
    }

private func setupCustomHeader(meeting: RealtimeKitClient) {
        let header = CustomHeaderView(meeting: meeting)
        self.customHeader = header

view.addSubview(header)
        header.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
            header.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            header.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            header.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }
}