Vaibhav Shinde

Making Good Grid

How I made a grid that's good.

Here’s the demo loaded from https://good-grid.vercel.app for you to play around and get a feel of the library.

While working at Dyte, I was tasked with building a new UI Kit, specifically designed for use with Dyte’s Core SDK, which would consist of a wide range of components which you can use to create a live audio/video experience and ship it in your product quickly.

A specific component I worked on which was most intriguing to me was the <dyte-simple-grid /> component, where I tried out various approaches to achieve a nice video call experience.

Our previous grid in our older sdk used normal CSS grid properties which was a straightforward approach.

For example, for 5 users, it would look like this:

5 participant grid in older SDK

And, for 3 users:

3 participant grid in older SDK

For a set of ranges of participant count, we needed to hard code the row and column css properties.

It is clear that each user doesn’t get the same space and to avoid empty spaces, some users’ tiles would take up too much or too little space.

A nicer grid would look like this, equal sizes (and aspect ratio) & equal spacing.

A nicer 5 participant grid

This required a two step solution:

  1. Calculate the grid item dimensions
  2. Position the items correctly in the container

Calculating dimensions

The goal is to calculate dimensions of n number of tiles which can be placed in a container, given an aspect ratio of each item and gap between the items.

A quick google search led me to this existing project: alicunde.github.io/Videoconference-Dish-CSS-JS/.

Spent some time experimenting with it, as the code needed to be optimized. I benchmarked it to see it’s performance, it took about ~ 0.5 to 2ms on each participant that got added or when the container was resized. And resizing it a bit on lower spec systems also was not a very fluid experience, so I tried to write some simpler logic, even though this would have just worked.

I tried to write my own implementations from scratch because this was an interesting problem to solve, and even got some of them working. But then I came across this answer on stackoverflow: stackoverflow.com/a/28268965.

I tried writing the logic from the answer in code, although with modifications to support arbitrary aspect ratios and it was even more efficient than my other implementations (that is anywhere between 4x to 20x faster). It felt really fluid on resizing as well, so it was natural for me to go with it. It’s efficiency is O(N), where N is number of grid items, although I think this can be optimized even further.

You can try it out at good-grid.vercel.app and play around with the number of tiles, aspect ratio and gap.

Positioning the items

If we’re looking for a simple solution, we could just align the items with the dimensions we received from the previous part in a flex container with the following styling:

.container {
  display: flex;
  align-content: center;
  justify-content: center;
  flex-wrap: wrap;
}

But the problem with this approach is, on resizing the statically positioned items will often overflow the container as the dimensions would go from a larger to a smaller value, which isn’t a very fluid experience.

A solution to that would be to position the tiles absolutely relative to the parent container, that way we can transition the items when their position changes, and there will no problem of an overflow.

.container {
  position: relative;
}

.grid-item {
  position: absolute;
}

This part I sketched out and wrote an implementation which is also pretty efficient: O(1).

Example

To use this in your app, simply install the package:

npm install good-grid

Here’s a minimal example of the full API usage in React:

import { createGrid } from 'good-grid';
import { useGridDimensions } from 'good-grid/react';

function App() {
  const $el = useRef();

  // hook that listens to resize of the element
  // and returns it's dimensions
  const dimensions = useGridDimensions($el);

  const { width, height, getPosition } = createGrid({
    dimensions,
    count: participants.length,
    aspectRatio: '16:9',
    gap: 10,
  });

  return (
    <div className="container" ref={$el}>
      {participants.map((participant, index) => {
        const { top, left } = getPosition(index);

        return (
          <div
            className="grid-item"
            style={{
              width,
              height,
              top,
              left,
              position: 'absolute',
              transition: '0.4s all',
            }}
            key={participant.name}
          >
            {participant.name}
          </div>
        );
      })}
    </div>
  );
}

getPosition is a function which accepts the index of the item and determines it’s position.

Easy and clean, isn’t it?

That wraps it up, go ahead and try it out in your project to get a nice grid experience :rocket: