On this page
React UI Kit for Meet Calls
@respiresoft/meet-react, React 18 and 19, built on the headless client.
Publishing. The package is not on npm yet. For a call you can ship today use the embed SDK.
It sits on the headless client: use <MeetRoom> for a complete screen, or the hooks and
components for your own layout.
Reference from the package README#
React components and hooks for Meet calls. They are built only on @respiresoft/meet-client, the
headless client, so a screen made with them gets last-N and simulcast, reconnecting and host rules for free.
No CSS framework: one small stylesheet, every colour, radius and font from CSS variables.
Use <MeetRoom> for a whole call screen, or the hooks and parts for your own layout. Use the embed
(@respiresoft/meet-embed) when you want Meet's own page in an iframe.
Install#
npm install @respiresoft/meet-react @respiresoft/meet-clientReact 18 or 19. ESM, with types. Your server mints the meeting token (see the server SDKs); the browser never sees an API key.
These packages are not on npm yet: until they are, install from the repository folders (sdk/client, sdk/react).
The one-line room#
import { MeetRoom } from '@respiresoft/meet-react';
import '@respiresoft/meet-react/style.css';
export default function Call({ token }) {
return <div style={{ height: '100dvh' }}><MeetRoom baseUrl="https://meet.example.com" token={token} /></div>;
}That gives the pre-join screen (camera preview, microphone and camera choice, a name box only if the token has no name),
the call (grid, controls, chat beside it, captions, reactions, a shared screen), a reconnecting banner, and an end screen.
The room fills its parent, so give the parent a height. onLeave(reason) runs once when the call ends. skipPreJoin
joins at once. theme is light, dark or auto; brand takes a white-label profile.
A custom layout from the hooks#
import { MeetProvider, useParticipants, useTracks, useChat, MediaView, AudioRenderer, ControlBar } from '@respiresoft/meet-react';
function Tile({ p }) {
const { video } = useTracks(p.id);
return <figure><MediaView track={video} /><figcaption>{p.name}{p.micEnabled ? '' : ' (muted)'}</figcaption></figure>;
}
function Screen() {
const people = useParticipants();
const { messages, send } = useChat();
return (
<>
{people.map((p) => <Tile key={p.id} p={p} />)}
<button onClick={() => send('hello')}>Say hello ({messages.length})</button>
<AudioRenderer />
<ControlBar />
</>
);
}
<MeetProvider baseUrl="https://meet.example.com" token={token}>
<Screen />
</MeetProvider>Nothing joins until client.join({ audio, video, name }) runs (useMeet().client). <MeetRoom> and <PreJoin> do it for you.
Pass your own client to <MeetProvider client={...}> to keep it outside React; a client the provider makes is left when it unmounts.
If you draw your own tiles, report their size so the engine sends the right video: useTileHint(ref, client, participant.id).
Hooks#
| Hook | Returns |
|---|---|
useMeet() |
{ client, state, role, canHost, permissions, recording, leftReason, error } |
useParticipants() |
everybody in the call, you first, then the others in join order (people not in the call yet are left out) |
useLocalParticipant() |
you |
useTracks(id) |
{ audio, video, screen, screenAudio } tracks of one person |
useActiveSpeaker() |
{ participant, level } of whoever was loudest last, or null |
useChat() |
{ messages, send(text) } |
useCaptions() |
{ enabled, setEnabled(on), lines }; lines show the translation into your caption language when there is one, and drop off after a few seconds |
useDevices() |
{ audioInput, videoInput, audioOutput, selected, select(kind, id), refresh, canChooseOutput } |
useHostActions() |
{ canHost, canRecord, recording, mute, disableCamera, lowerHand, remove, endMeeting, startRecording, stopRecording } |
useReactions() |
emoji that arrived in the last few seconds |
useTileHint(ref, client, id) |
reports a tile's height to setVideoQuality, null when it is off screen or gone |
Components#
| Component | What it is |
|---|---|
<MeetProvider client | baseUrl+token labels> |
gives the hooks and parts their client |
<MeetRoom> |
pre-join, call, chat and end screen in one |
<VideoGrid> |
tiles that fill the space, active speaker first, a shared screen large with a strip, pages of 24 (pageSize); every tile reports its size, tiles off screen or on another page report null |
<ParticipantTile participant source speaking> |
video or initials, name, muted and hand marks, speaking ring, host actions menu |
<ControlBar onToggleChat chatOpen unread onLeave> |
mic and camera with a device menu each, screen share, hand, reactions, chat, captions, host tools, leave; the less used buttons fold into "More" on a narrow bar; viewers get chat, captions and leave |
<ChatPanel onClose> |
messages and the box |
<CaptionsBar> |
the last few caption lines |
<PreJoin onJoin askName busy error> |
preview, devices, a name only when the token has none; onJoin({ name, audio, video, devices }) |
<AudioRenderer> |
plays every remote microphone and screen sound, on the speaker chosen with selectDevices({ audioOutput }) where the browser has setSinkId |
<ReactionLayer> |
floating emoji |
<MediaView track> |
a <video> fed from one track |
Every component takes labels for per-component strings.
Theming#
Import @respiresoft/meet-react/style.css, then set variables on .meet-root (or any parent). The names follow the
white-label brand profile keys (docs/WHITELABEL.md), with _ written as -:
| Variable | Brand key |
|---|---|
--meet-background, --meet-surface, --meet-border, --meet-text, --meet-muted |
colors.* |
--meet-accent, --meet-accent-text, --meet-danger |
colors.accent, colors.accent_text, colors.danger |
--meet-font |
font |
--meet-radius |
radius |
Dark colours apply with theme="dark", or on auto when the device is dark. brandVars(brand, 'light' | 'dark') turns a brand profile
into these variables; <MeetRoom brand={...}> does it for you. Also: --meet-gap (8px) and --meet-touch (44px). Layout works from 360 px;
touch targets are 44 px on touch screens; buttons have labels, menus close on Escape and move with the arrow keys.
Words#
English only. Pass labels (a partial object, see defaultLabels for every key) to change or translate any string, for example
labels={{ leave: 'Quitter', dir: 'rtl' }}. {name}, {n}, {page} and {pages} in a label are filled in.