Skip to main content
Showing who is speaking makes a call feel alive. Daily React gives you two tools: useActiveSpeakerId for the current speaker, and useAudioLevelObserver for continuous volume.

Highlight the active speaker

useActiveSpeakerId returns the session ID of the participant Daily considers the active speaker, or null when nobody has spoken:
import { useActiveSpeakerId, DailyVideo } from '@daily-co/daily-react';

function Tile({ sessionId }) {
  const activeSpeakerId = useActiveSpeakerId();
  const isSpeaking = sessionId === activeSpeakerId;
  return (
    <div style={{ outline: isSpeaking ? '3px solid limegreen' : 'none' }}>
      <DailyVideo sessionId={sessionId} type="video" />
    </div>
  );
}

Spotlight layouts

For a spotlight that follows the speaker, render the active speaker’s video directly. Pass ignoreLocal so your own voice does not steal the spotlight:
import { useActiveSpeakerId, DailyVideo } from '@daily-co/daily-react';

function Spotlight() {
  const activeSpeakerId = useActiveSpeakerId({ ignoreLocal: true });
  if (!activeSpeakerId) return <p>Waiting for someone to speak…</p>;
  return <DailyVideo sessionId={activeSpeakerId} type="video" fit="cover" />;
}
Pass a filter callback to restrict which participants can become the active speaker, for example to spotlight only presenters:
const activeSpeakerId = useActiveSpeakerId({
  filter: (sessionId) => presenterIds.includes(sessionId),
});

Build a volume meter

The active speaker is a coarse, on/off signal. For a continuous volume meter, useAudioLevelObserver calls you back with a number between 0 and 1 as a participant’s volume changes:
import { useAudioLevelObserver, useLocalSessionId } from '@daily-co/daily-react';
import { useCallback, useRef } from 'react';

function MicMeter() {
  const localSessionId = useLocalSessionId();
  const barRef = useRef(null);

  useAudioLevelObserver(
    localSessionId,
    useCallback((volume) => {
      // Drive the DOM directly; avoid setState on every frame.
      if (barRef.current) barRef.current.style.transform = `scaleY(${Math.max(0.05, volume)})`;
    }, [])
  );

  return <div ref={barRef} style={{ height: 40, width: 8, background: 'limegreen', transformOrigin: 'bottom' }} />;
}
Volume fires frequently. Write to a ref and mutate the DOM (as above) rather than calling setState on every change, which would re-render the component dozens of times a second. The callback must be memoized with useCallback, like all Daily React event callbacks.
Pass interval (minimum 100ms) to control how often the callback fires, and onError to handle browsers where the observer is unavailable.
The older useAudioLevel hook (which took a raw MediaStreamTrack) is deprecated as of 0.20.0. Use useAudioLevelObserver with a sessionId instead.

Speaking indicator, complete

A muted-aware speaking dot for a tile:
import { useCallback, useRef } from 'react';
import { useAudioLevelObserver, useMediaTrack } from '@daily-co/daily-react';

function SpeakingDot({ sessionId }) {
  const audio = useMediaTrack(sessionId, 'audio');
  const dotRef = useRef(null);

  useAudioLevelObserver(
    sessionId,
    useCallback((volume) => {
      if (dotRef.current) dotRef.current.style.opacity = String(Math.min(1, volume * 4));
    }, [])
  );

  if (audio.isOff) return <span>🔇</span>;
  return <span ref={dotRef} style={{ opacity: 0 }}>🟢</span>;
}

Next steps

Rendering media

The video and audio components.

Working with participants

Read more participant state.