Skip to content

Frontend Development

The Meet frontend is a TypeScript/React SPA built with Vite.

Tech stack

Component Technology
Framework React 18
Language TypeScript
Build tool Vite
WebRTC @livekit/components-react
Accessible UI React Aria (Adobe)
State management Zustand
i18n i18next
Testing Vitest + React Testing Library
Linting ESLint + Prettier

Running in dev mode

# Start backend services
make run-backend

# Install and start the frontend dev server
make frontend-development-install
make run-frontend-development

# Or directly
cd src/frontend
npm install
npm run dev

Dev server runs at http://localhost:5173 with hot module replacement.

Project structure

src/frontend/src/
├── api/              # Typed fetch wrappers for backend endpoints
├── components/       # Shared/reusable components
├── features/
│   ├── conference/   # In-meeting UI
│   │   ├── controls/ # Mic, camera, screen share, reactions
│   │   ├── layout/   # Video grid, speaker view
│   │   ├── chat/     # Chat panel
│   │   └── recording/# Recording / transcription panels
│   ├── home/         # Home page, room creation
│   └── settings/     # Settings panels
├── hooks/            # Custom React hooks
├── stores/           # Zustand state stores
├── i18n/             # Translation files (en.json, fr.json, …)
└── App.tsx           # Root component + routing

LiveKit connection

import { LiveKitRoom, VideoConference } from '@livekit/components-react';

// Token and URL come from GET /api/v1.0/rooms/{id}/token/
<LiveKitRoom token={token} serverUrl={url} connect>
  <VideoConference />
</LiveKitRoom>

State management

import { create } from 'zustand';

interface ConferenceStore {
  isRecording: boolean;
  setIsRecording: (v: boolean) => void;
}

export const useConferenceStore = create<ConferenceStore>((set) => ({
  isRecording: false,
  setIsRecording: (v) => set({ isRecording: v }),
}));

Accessibility

Meet uses React Aria for accessible component primitives. Rules:

  • All interactive elements must have an accessible name (aria-label or visible text)
  • State changes (mute/unmute, recording) must be announced via ARIA live regions
  • Focus management: opening dialogs moves focus in; closing returns focus to the trigger
  • Test every new interactive element with keyboard navigation
import { Button } from 'react-aria-components';

<Button
  aria-label={isMuted ? "Unmute microphone" : "Mute microphone"}
  aria-pressed={isMuted}
  onPress={toggleMute}
>
  {isMuted ? <MicOffIcon /> : <MicIcon />}
</Button>

Internationalization

  1. Add the key to en.json and fr.json (minimum)
  2. Use in components:
const { t } = useTranslation();
<span>{t('controls.mute')}</span>

Other languages are managed via Crowdin.

Running tests

make test-front

# Or directly
cd src/frontend
npm test
npm run test:coverage

Linting and formatting

make lint-front

# Or
cd src/frontend
npm run lint
npm run format

Build-time environment variables

Set as Docker build arguments (VITE_ prefix):

Variable Purpose
VITE_API_BASE_URL Not used in the published image — API calls use relative URLs
VITE_APP_TITLE App title shown in the browser tab

In the published lasuite/meet-frontend image, the frontend makes API calls using relative URLs (/api/v1.0/...). This means it works correctly as long as the SPA and the API are served from the same origin — which is exactly what the routing nginx in the frontend container provides.