Skip to content

React SDK

The @q9labs/chalk-react package provides a turnkey VideoConference component that handles the entire meeting lifecycle.

Installation

Terminal window
bun add @q9labs/chalk-react

Quick Start

import { ChalkProvider, VideoConference } from '@q9labs/chalk-react'
import '@q9labs/chalk-react/styles.css'
function App() {
return (
<ChalkProvider apiUrl="https://api.chalk.q9labs.ai" token={jwt}>
<VideoConference
roomId="room-123"
userName="Alice"
role="host"
features={{ recording: true, whiteboard: true }}
onEnd={(data) => console.log('Duration:', data.duration)}
onLeave={() => router.push('/')}
/>
</ChalkProvider>
)
}

VideoConference

Zero-config video conferencing. Handles the full flow: lobbyjoiningmeetingend.

┌─────────────────────────────────────────────────────────────┐
│ VideoConference │
├─────────────────────────────────────────────────────────────┤
│ Phase: lobby │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ PreJoinLobby │ │
│ │ - Camera preview │ │
│ │ - Device selection │ │
│ │ - Audio/video toggle │ │
│ │ - [Join] button │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ │
│ Phase: joining (loading state) │
│ ↓ │
│ Phase: meeting │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ MeetingRoom │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Video Grid / Spotlight / Sidebar │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Controls: mic | cam | share | record | leave │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ ┌─────────┐ │ │
│ │ │ Chat │ (sidebar panel) │ │
│ │ └─────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ │
│ Phase: end │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ EndScreen │ │
│ │ - Meeting summary │ │
│ │ - Duration, participant count │ │
│ │ - [Rejoin] [Go Home] │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Props

Required

PropTypeDescription
roomIdstringRoom identifier
userNamestringDisplay name for the local participant

Optional

PropTypeDefaultDescription
role"host" | "participant""participant"Host gets recording controls
featuresFeaturesAll enabledEnable/disable features
defaultsDefaults-Initial UI states
themeTheme-Visual customization
soundsbooleantrueEnable sound effects
debugbooleanfalseEnable debug logging
slotsSlots-Custom UI slots
classNamestring-CSS class for container

Features

Each feature can be a boolean or a function returning boolean based on context.

interface Features {
chat?: FeatureValue
recording?: FeatureValue
screenShare?: FeatureValue
whiteboard?: FeatureValue
reactions?: FeatureValue
handRaise?: FeatureValue
tour?: FeatureValue
}
type FeatureValue = boolean | ((ctx: FeatureContext) => boolean)
interface FeatureContext {
participants: readonly Participant[]
localParticipant: Participant | null
participantCount: number
isRecording: boolean
}

Example: Conditional features

<VideoConference
roomId="room-123"
userName="Alice"
features={{
recording: ({ participantCount }) => participantCount >= 2,
screenShare: true,
whiteboard: false,
}}
/>

Defaults

interface Defaults {
layout?: "grid" | "spotlight" | "sidebar"
audioEnabled?: boolean
videoEnabled?: boolean
chatOpen?: boolean
participantsOpen?: boolean
}

Theme

interface Theme {
accentColor?: string
borderRadius?: "rounded" | "sharp"
}

Slots

Override or wrap default UI components.

interface Slots {
header?: ReactNode | ((DefaultHeader: ComponentType) => ReactNode)
controls?: ReactNode | ((DefaultControls: ComponentType) => ReactNode)
sidebar?: ReactNode | ((DefaultSidebar: ComponentType) => ReactNode)
videoGrid?: ReactNode | ((DefaultVideoGrid: ComponentType) => ReactNode)
lobby?: {
header?: ReactNode
footer?: ReactNode
}
endScreen?: {
actions?: ReactNode
}
}

Callbacks

onJoin

Fires when the user successfully joins the room.

onJoin?: (data: MeetingJoinedData) => void
interface MeetingJoinedData {
roomId: string
participantId: string
role: string
displayName: string
isRecording: boolean
joinedAt: Date
}

onLeave

Fires when the user initiates leaving (clicks leave button).

onLeave?: () => void

onEnd

Fires when the meeting ends with full session data and stats.

onEnd?: (data: MeetingEndData) => void
interface MeetingEndData {
roomId: string
duration: number // seconds
transcripts: Transcript[]
recordingId: string | null
participantCount: number // peak concurrent
totalParticipants: number // unique participants
participants: ParticipantSession[]
hostId: string | null
startedAt: Date
endedAt: Date
stats: MeetingStats
}
interface ParticipantSession {
id: string
externalId: string | null
displayName: string
role: "host" | "participant"
joinedAt: Date
leftAt: Date | null
}
interface MeetingStats {
chatMessageCount: number
reactionCount: number
handRaiseCount: number
screenShareCount: number
whiteboardOpened: boolean
recordingDuration: number // seconds
}

onError

Fires when an error occurs.

onError?: (error: ChalkError) => void

onAddPeople

Fires when the invite/add people button is clicked.

onAddPeople?: () => void

Full Example

import { ChalkProvider, VideoConference } from '@q9labs/chalk-react'
import type { MeetingEndData, MeetingJoinedData, ChalkError } from '@q9labs/chalk-react'
import '@q9labs/chalk-react/styles.css'
function MeetingPage({ roomId, token }: { roomId: string; token: string }) {
const router = useRouter()
const handleJoin = (data: MeetingJoinedData) => {
console.log(`Joined as ${data.displayName} (${data.role})`)
analytics.track('meeting_joined', { roomId: data.roomId })
}
const handleEnd = (data: MeetingEndData) => {
console.log(`Meeting ended after ${data.duration}s`)
console.log(`Participants: ${data.totalParticipants}`)
console.log(`Chat messages: ${data.stats.chatMessageCount}`)
if (data.recordingId) {
console.log(`Recording available: ${data.recordingId}`)
}
// Save transcripts
if (data.transcripts.length > 0) {
saveTranscripts(data.transcripts)
}
}
const handleError = (error: ChalkError) => {
toast.error(error.message)
}
return (
<ChalkProvider apiUrl="https://api.chalk.q9labs.ai" token={token}>
<VideoConference
roomId={roomId}
userName="Alice"
role="host"
features={{
chat: true,
recording: true,
screenShare: true,
whiteboard: true,
reactions: true,
handRaise: true,
}}
defaults={{
layout: "grid",
audioEnabled: true,
videoEnabled: true,
}}
theme={{
accentColor: "#6366f1",
borderRadius: "rounded",
}}
sounds={true}
onJoin={handleJoin}
onEnd={handleEnd}
onLeave={() => router.push('/')}
onError={handleError}
onAddPeople={() => openInviteModal()}
/>
</ChalkProvider>
)
}

What It Includes

The VideoConference component provides a complete meeting experience:

  • Pre-join lobby - Camera/mic preview, device selection, display name input
  • Video grid - Adaptive layouts (grid, spotlight, sidebar)
  • Control bar - Mute, camera, screen share, recording, reactions, hand raise
  • Chat panel - Real-time messaging with unread notifications
  • Whiteboard - Collaborative drawing (when enabled)
  • End screen - Meeting summary with duration and stats
  • Sound effects - Audio cues for join, leave, messages, reactions