Video Call Grid — Free HTML CSS JS UI Snippet

Video Call Grid · Dashboards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Auto-fit responsive grid
repeat(auto-fit, minmax(150px, 1fr)) reflows any participant count from four columns down to one with zero media queries.
Click-to-pin spotlight
One class switch turns the grid into a 16:9 spotlight with a scrollable filmstrip of the remaining participants.
Active-speaker ring
The speaking tile gets a green border, box-shadow glow, and a staggered scaleY equalizer animation.
Per-participant mute badges
Name pills overlay each tile with an on/off mic SVG driven by each participant's muted flag.
Initials avatars
Camera-off tiles render generated initials over per-person gradients — no image assets required.
Working control bar
Mic and camera buttons toggle state with aria-pressed; mute updates your tile badge and speaking eligibility live.
Stable tile shapes
aspect-ratio keeps 4:3 tiles (16:9 when pinned) so the layout never collapses regardless of content.
SDK-ready structure
One render function over a participants array maps directly onto LiveKit, Twilio, Agora, or Daily participant events.

About this UI Snippet

Video Call Grid — Zoom-Style Participant Grid with Active Speaker and Spotlight Pinning

Screenshot of the Video Call Grid snippet rendered live

Every video conferencing product — Zoom, Google Meet, Teams, Discord — converges on the same two layouts: an equal-sized participant grid, and a spotlight view where one tile dominates while the rest collapse into a filmstrip. This component recreates that pattern in plain HTML, CSS, and vanilla JavaScript: a responsive tile grid with animated active-speaker highlighting, per-participant mute badges, click-to-pin spotlight switching, and a Meet-style control bar with mic, camera, and leave buttons.

The auto-fit grid

The participant layout is a single CSS rule: grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)). With six participants in a 680px container this naturally produces rows of three or four tiles, and as the container narrows it reflows to two columns and then one — no media queries and no JavaScript layout math. Each tile keeps a stable shape with aspect-ratio: 4/3, which is how real call UIs keep camera-off tiles from collapsing to zero height.

Spotlight mode with one class

Clicking a tile sets pinnedIndex and re-renders with a spotlight class on the grid. That class does three things in pure CSS: switches the grid to a single 1fr column, hides every tile except the .pinned one, and reveals the .vcg-strip filmstrip of miniature tiles for the other participants. The pinned tile also widens to aspect-ratio: 16/9, matching how Meet enlarges a presented feed. Because layout switching is class-driven, the JavaScript stays tiny — it only tracks which index is pinned and rebuilds the markup.

Active-speaker simulation

A setInterval picks a random unmuted participant every 2.2 seconds and marks their tile .speaking, which lights a green border, an outer glow via box-shadow, and a three-bar equalizer in the corner. The equalizer is three <i> elements animated with a staggered scaleY keyframe (animation-delay: 0s / 0.15s / 0.3s), the same trick used in the audio waveform visualizer. In a real app you would replace the interval with the audioLevel events your WebRTC SDK (LiveKit, Twilio, Agora, Daily) already emits — the rendering layer stays identical.

Mute badges and the control bar

Each name pill overlays the tile bottom-left with position: absolute, a translucent rgba background, and backdrop-filter: blur(4px) so it stays readable over any tile colour. The mic icon inside swaps between an "on" and a slashed "off" SVG based on each participant's muted flag. The control bar's mic button toggles *your* participant's flag and re-renders, so your own tile's badge and speaking eligibility update instantly; buttons expose their state through aria-pressed so screen readers announce mute correctly.

Avatars without images

Tiles show initials avatars generated from each name — initials() splits on spaces and takes the first letter of the first two words — over a per-person gradient built from their accent colour plus a dark base. This is exactly how production call UIs render camera-off participants, and it keeps the snippet dependency-free. To show real video, replace the avatar div with a <video> element fed by a MediaStream; every other part of the component (grid, pinning, speaking ring, badges) works unchanged.

Customisation

Add or remove entries in the PEOPLE array — the auto-fit grid absorbs any count. Swap the accent colours, wire the leave button to your session teardown, and drive speakingIndex and muted from your SDK's events instead of the demo interval. The whole UI is one render function, so porting it onto live data is a matter of replacing the simulated state.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of tracing the layout logic by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)) reflows six tiles into different row counts as the container narrows without any media query, and how the single "spotlight" class toggle on the grid cascades through CSS to hide tiles, resize the pinned one, and reveal the filmstrip all at once. It's a good optimization target too — ask whether rebuilding the entire grid's innerHTML string on every render() call (including every 2.2-second speaker change) is wasteful compared to updating only the classes that actually changed. For extending it, have it wire the demo setInterval speaker simulation to real WebRTC audioLevel events, add a raise-hand indicator per tile, or support dragging tiles to reorder them. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a Zoom-style video call participant grid with pin-to-spotlight support, in plain HTML, CSS, and vanilla JavaScript with no libraries.

Requirements:
- A responsive grid of participant tiles using grid-template-columns: repeat(auto-fit, minmax(Npx, 1fr)) so the number of columns automatically adapts to container width and participant count with zero media queries; each tile keeps a fixed aspect-ratio so tiles never collapse to zero height.
- Each tile shows an initials avatar generated from the participant's name (not an image), rendered over a per-person colored gradient background, plus a name pill overlaid at the bottom-left with a translucent, blurred background and a mic icon that switches between an "on" and a slashed "off" SVG based on that participant's muted state.
- A single boolean-like "pinned index" state: clicking a tile toggles whether it's pinned. When something is pinned, the grid must switch to a single-column spotlight layout (the pinned tile enlarges to a widescreen aspect ratio), all other tiles must hide from the main grid, and a horizontally-scrollable filmstrip of small thumbnail tiles for the remaining participants must appear below it. Clicking the pinned tile again returns to the normal equal-sized grid.
- A simulated "active speaker" mechanism: on an interval, randomly select one currently-unmuted participant and give their tile a distinct glowing border plus a small multi-bar equalizer animation in the corner, built from a few bars with staggered animation-delay values on a shared scaleY keyframe.
- A control bar with mic and camera toggle buttons that reflect their pressed/off state via aria-pressed and a distinct visual style, plus a "leave call" button styled in a warning color; toggling your own mic must update your own tile's mute badge and remove your tile from active-speaker eligibility while muted.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste the HTML, CSS, and JSA six-person call grid renders with initials avatars, name pills with mic badges, and a control bar with mic, camera, and leave buttons.
  2. 2
    Watch the active speakerEvery 2.2 seconds a random unmuted participant gets the green speaking ring and an animated three-bar equalizer in their tile corner.
  3. 3
    Click a tile to pin itThe grid switches to spotlight mode — the pinned tile expands to 16:9 and the other participants collapse into a scrollable filmstrip below.
  4. 4
    Click the big tile to unpinSpotlight mode toggles off and the equal grid returns; the hint text under the control bar tells you which mode you are in.
  5. 5
    Toggle your mic and cameraThe mic button mutes the "You" participant — the badge on your tile swaps to a slashed mic and you stop receiving the speaking ring.
  6. 6
    Wire it to real dataReplace the PEOPLE array and the demo interval with your WebRTC SDK's participant list and audioLevel events; swap avatars for video elements.

Real-world uses

Common Use Cases

Video conferencing apps
The participant layout for a WebRTC call — pair it with a floating chat widget for in-call messaging.
Virtual classroom UIs
Pin the teacher in spotlight while students sit in the filmstrip; combine with a poll widget for live questions.
Team standup tools
Show who is talking during async or live standups next to a team presence list.
Webinar and livestream studios
Use spotlight mode as the "program view" of the current presenter with co-hosts in miniatures.
Product landing pages
Drop the animated grid into a hero as a believable in-product screenshot alternative — see the app hero.
Learning the layout pattern
A compact reference for auto-fit grids, class-driven layout switching, and active-speaker rendering.

Got questions?

Frequently Asked Questions

The container uses grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)), so the browser packs as many 150px-minimum columns as fit and stretches them evenly. Six people become a 3×2 grid on desktop and a single column on narrow screens automatically. Add a seventh entry to the PEOPLE array and it just flows in — there is no per-count layout code, which is why real call UIs use the same technique.

Clicking a tile stores its index in pinnedIndex and re-renders with a spotlight class on the grid. CSS does the rest: the grid becomes one column, .vcg-tile:not(.pinned) is hidden, and the filmstrip strip is revealed with miniature tiles for everyone else. Clicking the pinned tile resets pinnedIndex to -1. Keeping the mode in a class means the layouts live entirely in the stylesheet.

Replace the PEOPLE array with the participant list from your SDK (LiveKit, Twilio Video, Agora, Daily) and call render() on join/leave events. Drive speakingIndex from the SDK's active-speaker or audioLevel events instead of the demo setInterval, and set each participant's muted flag from track-mute events. For live video, render a <video> element bound to the participant's MediaStream in place of the initials avatar.

The snippet has no microphone access, so an interval that promotes a random unmuted participant every 2.2 seconds stands in for real audio levels. The important part is the rendering contract: whoever holds speakingIndex gets the ring and equalizer. In production you keep that contract and just change who writes the value — typically a threshold over the audioLevel your SDK reports per participant.

Yes — it maps cleanly onto components. Hold participants, pinnedIndex, and speakingIndex in state (useState / ref() / component fields), render tiles with a map instead of innerHTML strings, and attach the pin handler per tile. The demo speaker interval belongs in useEffect / onMounted / ngOnInit with a cleanup that clears it. All the CSS — auto-fit grid, spotlight class, equalizer keyframes — ports unchanged.