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:
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.
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)
.
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 🚀