JavaScript Video Conference Grid Layout Library with Gallery & Spotlight Modes

JavaScript Video Conference Grid Layout Library with Gallery & Spotlight Modes
JavaScript Video Conference Grid Layout Library with Gallery & Spotlight Modes
Meet Layout Grid is a responsive, framework-agnostic grid library designed for video conference layouts with smooth motion animations.

Features:

  • Dual Layout Modes: Gallery mode with flexible grid packing and optional pin mode that splits the view into focus area plus thumbnails. Spotlight mode displays a single participant at full size.
  • Pin and Focus Support: Pin any participant to become the main view. The library repositions other participants into a thumbnail strip on the right, left, top, or bottom.
  • Spring-Based Animations: Uses Motion (Framer Motion for React, Motion One for Vue) to create smooth transitions when participants join, leave, or change position.
  • Pagination System: Split large participant groups across multiple pages. Supports separate pagination for the main grid and the thumbnail strip in pin mode.
  • Max Visible with Overflow Indicator: Limit visible participants and show a “+N more” indicator on the last visible item.
  • Flexible Aspect Ratios: Define different aspect ratios per participant. The library handles mixed ratios like 16:9 desktop and 9:16 mobile in a single grid.
  • Floating Picture-in-Picture: Draggable PiP component that snaps to corners. Includes responsive breakpoints that auto-adjust size based on container width.
  • Grid Overlay System: Full-screen overlay for screen sharing or whiteboard content that sits above the participant grid.
  • Responsive Design: The grid recalculates on container resize. Uses justified packing algorithm to minimize wasted space.
  • Framework Support: Core package is framework-agnostic. React and Vue packages provide components, hooks, and composables.
  • TypeScript Native: Ships with complete type definitions for all APIs, options, and return values.
  • Tree-Shakeable: Import only the features you need. The core package has zero dependencies.

Use Cases:

  • Video Conferencing Applications: Build Zoom-style grid layouts with gallery view, spotlight mode, and pin functionality.
  • Virtual Classroom Platforms: Display students in a grid with the teacher pinned to the main area.
  • Webinar Interfaces: Spotlight the speaker while showing audience members in a thumbnail strip.
  • Multi-Device Support: Handle participants joining from phones (9:16 portrait) and desktops (16:9 landscape) in the same grid.

See It In Action:

Basic Usage:

1. Install the package for your framework:

The React and Vue packages re-export everything from core. You do not need to install core separately when using a framework package.

# Vanilla JavaScript/TypeScript
npm install @thangdevalone/meet-layout-grid-core

# React
npm install @thangdevalone/meet-layout-grid-react

# Vue 3+
npm install @thangdevalone/meet-layout-grid-vue

2. Basic usage (Vanilla JavaScript):

import { createMeetGrid } from '@thangdevalone/meet-layout-grid-core'

// Create a grid calculator with your container dimensions
const grid = createMeetGrid({
  dimensions: { width: 800, height: 600 },  // Container size
  count: 6,                                  // Number of items
  aspectRatio: '16:9',                       // Item aspect ratio
  gap: 8,                                    // Gap between items
  layoutMode: 'gallery',                     // Layout mode
})

// Position each item using absolute positioning
for (let i = 0; i < 6; i++) {
  const { top, left } = grid.getPosition(i)
  const { width, height } = grid.getItemDimensions(i)
  
  // Apply styles to your video element
  element.style.cssText = `
    position: absolute;
    top: ${top}px;
    left: ${left}px;
    width: ${width}px;
    height: ${height}px;
  `
}

// Recalculate when container resizes
window.addEventListener('resize', () => {
  const newGrid = createMeetGrid({
    dimensions: { 
      width: container.clientWidth, 
      height: container.clientHeight 
    },
    count: 6,
    aspectRatio: '16:9',
    gap: 8,
    layoutMode: 'gallery',
  })
  
  // Update positions...
})

3. Basic usage (React):

import { GridContainer, GridItem } from '@thangdevalone/meet-layout-grid-react'
function MeetingGrid({ participants }) {
  return (
    // GridContainer manages the grid layout and animations
    <GridContainer 
      aspectRatio="16:9"      // Default aspect ratio for all items
      gap={8}                 // Gap between items in pixels
      layoutMode="gallery"    // Use flexible grid layout
      count={participants.length}  // Total number of participants
    >
      {participants.map((p, index) => (
        // GridItem handles positioning and transitions for each participant
        <GridItem key={p.id} index={index}>
          <VideoTile participant={p} />
        </GridItem>
      ))}
    </GridContainer>
  )
}

4. Basic usage (Vue):

<script setup>
import { GridContainer, GridItem } from '@thangdevalone/meet-layout-grid-vue'
import { ref } from 'vue'
const participants = ref([
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  // ... more participants
])
const pinnedIndex = ref(null)
function pinParticipant(index) {
  pinnedIndex.value = index
}
</script>
<template>
  <GridContainer 
    aspect-ratio="16:9"
    :gap="8"
    :count="participants.length"
    layout-mode="gallery"
    :pinned-index="pinnedIndex"
    others-position="right"
  >
    <GridItem 
      v-for="(p, index) in participants" 
      :key="p.id" 
      :index="index"
    >
      <VideoTile 
        :participant="p"
        @pin="pinParticipant(index)"
      />
    </GridItem>
  </GridContainer>
</template>

5. All configuration options.

  • dimensions ({ width: number, height: number }): Container size in pixels. Required for Vanilla JS API.
  • count (number): Total number of participants. Required.
  • aspectRatio (string): Default aspect ratio for all items. Accepts "16:9", "9:16", "4:3", or "auto". Default is "16:9".
  • gap (number): Gap between grid items in pixels. Default is 8.
  • layoutMode (‘gallery’ | ‘spotlight’): Layout mode. Gallery creates a flexible grid. Spotlight shows a single participant at full size. Default is 'gallery'.
  • pinnedIndex (number): Index of the pinned participant in gallery mode. When set, the layout splits into a focus area (pinned item) and thumbnails area (other participants). Default is undefined.
  • othersPosition (‘left’ | ‘right’ | ‘top’ | ‘bottom’): Position of thumbnails in pin mode. Default is 'right'.
  • maxItemsPerPage (number): Maximum items per page in gallery mode. Set to 0 to disable pagination. Default is 0.
  • currentPage (number): Current page index for gallery pagination. Zero-based. Default is 0.
  • maxVisible (number): Maximum visible items in the thumbnails area when using pin mode. Set to 0 to show all. Default is 0.
  • currentVisiblePage (number): Current page index for thumbnail pagination in pin mode. Zero-based. Default is 0.
  • itemAspectRatios ((string | undefined)[]): Array of aspect ratios for each participant. Use undefined to fall back to global aspectRatio. Length should match count.
  • floatWidth (number): Width of the auto-float PiP in 2-person mode. Overrides floatBreakpoints. Default is 120.
  • floatHeight (number): Height of the auto-float PiP in 2-person mode. Overrides floatBreakpoints. Default is 160.
  • floatBreakpoints (PipBreakpoint[]): Responsive breakpoints for auto-float PiP sizing. Each breakpoint is { minWidth: number, width: number, height: number }.
  • springPreset (‘snappy’ | ‘smooth’ | ‘gentle’ | ‘bouncy’): Animation preset for spring transitions. Default is 'smooth'.

6. API methods.

// Get the absolute position of an item in the grid
// Returns { top: number, left: number }
const position = grid.getPosition(index);

// Get the dimensions of a grid cell
// Returns { width: number, height: number }
const cellSize = grid.getItemDimensions(index);

// Get content dimensions with aspect ratio preserved
// Returns { width, height, offsetTop, offsetLeft }
// The offsets center the content within the cell
const content = grid.getItemContentDimensions(index, '16:9');

// Check if an item is visible on the current page
// Returns boolean
const visible = grid.isItemVisible(index);

// Check if an item is the main/pinned item
// Returns boolean
const isMain = grid.isMainItem(index);

// Get the index of the last visible item in the thumbnails area
// Returns number
const lastIndex = grid.getLastVisibleOthersIndex();

7. GridContainer Props (React/Vue):

// Get pagination information
// Returns PaginationInfo object:
// {
//   enabled: boolean,
//   currentPage: number,
//   totalPages: number,
//   itemsOnPage: number,
//   startIndex: number,
//   endIndex: number
// }
const pagination = grid.pagination;

// Get the number of hidden items when using maxVisible
// Returns number
const hidden = grid.hiddenCount;

8. FloatingGridItem Props:

  • children (ReactNode | VNode): Content to render inside the floating item. Required.
  • width (number): Width of the floating item in pixels. Overridden by breakpoints when provided. Default is 120.
  • height (number): Height of the floating item in pixels. Overridden by breakpoints when provided. Default is 160.
  • breakpoints (PipBreakpoint[]): Responsive breakpoints for auto-sizing. Each breakpoint is { minWidth: number, width: number, height: number }.
  • initialPosition ({ x: number, y: number }): Extra offset from the anchor corner. Default is { x: 16, y: 16 }.
  • anchor (‘top-left’ | ‘top-right’ | ‘bottom-left’ | ‘bottom-right’): Which corner to snap the item to. Default is 'bottom-right'.
  • visible (boolean): Whether the floating item is visible. Default is true.
  • edgePadding (number): Minimum padding from container edges in pixels. Default is 12.
  • onAnchorChange ((anchor: string) => void): Callback when anchor corner changes after drag.
  • transition (Transition): Custom Motion transition object for snap animation. Default is spring with stiffness 400 and damping 30.
  • borderRadius (number): Border radius of the floating item in pixels. Default is 12.
  • boxShadow (string): CSS box-shadow value. Default is '0 4px 20px rgba(0,0,0,0.3)'.
  • className (string): Additional CSS class name.
  • style (CSSProperties | object): Custom inline styles. Merged with floating styles.

9. GridOverlay Props:

  • children (ReactNode | VNode): Content to render inside the overlay. Required when visible is true.
  • visible (boolean): Whether to show the overlay. Default is true.
  • backgroundColor (string): Overlay background color. Accepts any valid CSS color. Default is 'rgba(0,0,0,0.5)'.

10. GridItem Render Props (React):

<GridItem index={0}>
  {({ isLastVisibleOther, hiddenCount }) => (
    // Your component here
  )}
</GridItem>
  • isLastVisibleOther (boolean): True if this is the last visible item in the thumbnails area when using maxVisible.
  • hiddenCount (number): Number of hidden items when using maxVisible. Use this to show “+N more” indicators.

Advanced Usage:

1. Pagination for Large Groups:

import { GridContainer, GridItem } from '@thangdevalone/meet-layout-grid-react'
import { useState } from 'react'
function MeetingGrid({ participants }) {
  const [currentPage, setCurrentPage] = useState(0)
  const maxItemsPerPage = 9  // Show 9 participants per page
  return (
    <>
      <GridContainer
        count={participants.length}
        maxItemsPerPage={maxItemsPerPage}  // Enable pagination
        currentPage={currentPage}          // Current page index (0-based)
      >
        {participants.map((p, index) => (
          <GridItem key={p.id} index={index}>
            <VideoTile participant={p} />
          </GridItem>
        ))}
      </GridContainer>
      {/* Pagination controls */}
      <div>
        <button 
          onClick={() => setCurrentPage(prev => Math.max(0, prev - 1))}
          disabled={currentPage === 0}
        >
          Previous
        </button>
        <span>Page {currentPage + 1}</span>
        <button 
          onClick={() => setCurrentPage(prev => prev + 1)}
          disabled={(currentPage + 1) * maxItemsPerPage >= participants.length}
        >
          Next
        </button>
      </div>
    </>
  )
}

For pin mode, use maxVisible and currentVisiblePage to paginate the thumbnail strip:

<GridContainer
  layoutMode="gallery"
  pinnedIndex={0}
  maxVisible={4}                    // Show 4 thumbnails at a time
  currentVisiblePage={othersPage}   // Current page for thumbnails
>

2. Limit visible participants and show overflow count:

<GridContainer maxVisible={4} count={12}>
  {participants.map((p, index) => (
    <GridItem key={p.id} index={index}>
      {/* GridItem passes isLastVisibleOther and hiddenCount as render props */}
      {({ isLastVisibleOther, hiddenCount }) => (
        <>
          {isLastVisibleOther && hiddenCount > 0 ? (
            // Show "+N more" indicator on the last visible thumbnail
            <div className="more-indicator">
              +{hiddenCount} more
            </div>
          ) : (
            // Show normal video tile
            <VideoTile participant={p} />
          )}
        </>
      )}
    </GridItem>
  ))}
</GridContainer>

3. Handle mixed aspect ratios for phone and desktop participants. Supported aspect ratio values:

  • "16:9": Standard landscape video.
  • "9:16": Portrait video for mobile devices.
  • "4:3": Classic tablet ratio.
  • "auto": Stretch to fill the cell (ignores aspect ratio).
  • undefined: Use the global aspectRatio prop.
const participants = [
  { id: 1, name: 'Alice', device: 'desktop' },
  { id: 2, name: 'Bob', device: 'phone' },
  { id: 3, name: 'Charlie', device: 'desktop' },
]
// Define aspect ratio for each participant
const itemAspectRatios = participants.map(p => {
  if (p.device === 'phone') return '9:16'    // Portrait
  if (p.device === 'desktop') return '16:9'  // Landscape
  return undefined  // Use global aspectRatio
})
<GridContainer
  aspectRatio="16:9"               // Global default
  itemAspectRatios={itemAspectRatios}  // Per-item overrides
  count={participants.length}
>
  {participants.map((p, index) => (
    <GridItem key={p.id} index={index}>
      <VideoTile participant={p} />
    </GridItem>
  ))}
</GridContainer>

4. Add a draggable PiP that snaps to corners. The anchor prop controls which corner the PiP snaps to:

  • 'top-left'
  • 'top-right'
  • 'bottom-left'
  • 'bottom-right'
import { GridContainer, GridItem, FloatingGridItem } from '@thangdevalone/meet-layout-grid-react'
import { useState } from 'react'
function MeetingGrid({ participants, floatingParticipant }) {
  const [pipAnchor, setPipAnchor] = useState('bottom-right')
  const [showPip, setShowPip] = useState(true)
  return (
    <GridContainer count={participants.length}>
      {/* Main grid items */}
      {participants.map((p, index) => (
        <GridItem key={p.id} index={index}>
          <VideoTile participant={p} />
        </GridItem>
      ))}
      {/* Floating PiP component */}
      <FloatingGridItem
        width={130}                  // PiP width in pixels
        height={175}                 // PiP height in pixels
        anchor={pipAnchor}           // Corner to snap to
        visible={showPip}            // Show/hide PiP
        edgePadding={12}             // Minimum padding from edges
        borderRadius={8}             // Corner rounding
        onAnchorChange={setPipAnchor}  // Called when user drags to new corner
      >
        <VideoTile participant={floatingParticipant} />
      </FloatingGridItem>
    </GridContainer>
  )
}

5. Use breakpoints to auto-adjust PiP size based on container width. The default breakpoints provide 5 responsive levels:

Container WidthPiP Size
0 – 479px100 × 135
480 – 767px130 × 175
768 – 1023px160 × 215
1024 – 1439px180 × 240
1440px+220 × 295
import { 
  FloatingGridItem, 
  DEFAULT_FLOAT_BREAKPOINTS 
} from '@thangdevalone/meet-layout-grid-react'
// Use built-in breakpoints (5 levels from 100x135 to 220x295)
<FloatingGridItem breakpoints={DEFAULT_FLOAT_BREAKPOINTS}>
  <VideoTile participant={pip} />
</FloatingGridItem>
// Define custom breakpoints
const customBreakpoints = [
  { minWidth: 0, width: 80, height: 110 },       // Small screens
  { minWidth: 600, width: 150, height: 200 },    // Medium screens
  { minWidth: 1200, width: 250, height: 330 },   // Large screens
]
<FloatingGridItem breakpoints={customBreakpoints}>
  <VideoTile participant={pip} />
</FloatingGridItem>

For the auto-float PiP in 2-person mode, use floatBreakpoints on GridContainer:

<GridContainer 
  count={2} 
  floatBreakpoints={DEFAULT_FLOAT_BREAKPOINTS}
>
  {participants.map((p, i) => (
    <GridItem key={p.id} index={i}>
      <VideoTile participant={p} />
    </GridItem>
  ))}
</GridContainer>

6. Show full-screen content above the grid:

import { GridContainer, GridItem, GridOverlay } from '@thangdevalone/meet-layout-grid-react'
import { useState } from 'react'
function MeetingGrid({ participants, screenShare }) {
  const [isScreenSharing, setIsScreenSharing] = useState(false)
  return (
    <GridContainer count={participants.length}>
      {/* Grid items */}
      {participants.map((p, index) => (
        <GridItem key={p.id} index={index}>
          <VideoTile participant={p} />
        </GridItem>
      ))}
      
      {/* Full-screen overlay for screen sharing */}
      <GridOverlay 
        visible={isScreenSharing}            // Show/hide overlay
        backgroundColor="rgba(0, 0, 0, 0.9)" // Semi-transparent background
      >
        <ScreenShareView stream={screenShare} />
      </GridOverlay>
    </GridContainer>
  )
}

7. Control the animation style with presets:

PresetUse Case
snappyFast UI feedback
smoothLayout changes (default)
gentleSubtle motion
bouncySlight overshoot
<GridContainer springPreset="smooth">
  {/* items */}
</GridContainer>

The post JavaScript Video Conference Grid Layout Library with Gallery & Spotlight Modes appeared first on CSS Script.


Discover more from RSS Feeds Cloud

Subscribe to get the latest posts sent to your email.

Discover more from RSS Feeds Cloud

Subscribe now to keep reading and get access to the full archive.

Continue reading