DEV Community

Cover image for ZOOM-like video gallery with CSS Houdini 🎩
Anton Dosov
Anton Dosov

Posted on • Edited on • Originally published at adosov.dev

ZOOM-like video gallery with CSS Houdini 🎩

TLDR

Complete solution is here.

⚠️ This demo uses experimental API. Check browser support before using it in production.

If you use Chrome, make sure you have experimental-web-platform-features flag enabled. Check support for other browsers here.

Video in case you use a browser without CSS Layout API support:

Intro

Hi DEV community 👋

Last week I've built a video gallery just like in ZOOM.

I tried to find a solution using pure CSS but failed. This week I still don't have a pure CSS solution. Instead, I rebuilt the video gallery using experimental CSS Layout API from CSS Houdini 🎩.

Problem

ZOOM gallery view
image from zoom.us

Having videoCount videos with fixed aspectRatio and fixed container size (containerWidth, containerHeight), fit all the videos inside the container to occupy as much area as possible. Videos should have the same size and can't overflow the container.

CSS Houdini 🎩

CSS Houdini is a set of experimental browser APIs which allow to hook into the browser rendering process. We are going to use CSS Layout API for positioning and sizing video elements.

⚠️ This API is available only with an experimental flag. So it can't be used in production just yet!

Solution

Starting from following HTML structure:

<div id="gallery">
  <div class="video-container">
    <video/>
  </div>
  <div class="video-container">
    <video/>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

And making sure #gallery takes up the whole screen:

body {
  margin: 0;
  height: 100vh;
}
#gallery {
  height: 100%;
}
Enter fullscreen mode Exit fullscreen mode

display: layout(zoom-like-gallery)

This is the moment where Houdini 🎩 does his magic:

#gallery {
  height: 100%;
  display: layout(zoom-like-gallery); // 💥
}
Enter fullscreen mode Exit fullscreen mode

Normally we would use display property with one of the predefined values. Like grid, flex or inline-block. But CSS Layout API allows developers to implement their custom layouts 😱. And we are going to implement our custom zoom-like-gallery layout.

// check for CSS Layout API support
if ("layoutWorklet" in CSS) {
  // import a module with our custom layout
  CSS.layoutWorklet.addModule("zoom-like-gallery-layout.js");
}
Enter fullscreen mode Exit fullscreen mode

Then in zoom-like-gallery-layout.js we register a layout:

registerLayout(
  "zoom-like-gallery",
  class {
    // array of CSS custom properties that belong to the container (to the `#gallery` in our case)
    // look at this like at parameters for custom layout
    // we will use this later to make aspect ratio configurable from CSS 
    static get inputProperties() {
      return [];
    }

    // array of CSS custom properties that belong to children (to `.video-container` elements in our case).
    static get childrenInputProperties() {
      return [];
    }

    // properties for layout,
    // see: https://drafts.css-houdini.org/css-layout-api/#dictdef-layoutoptions 
    static get layoutOptions() {  }

    // allows to define min-content / max-content sizes for a container (for `#gallery` in our case).
    // see: https://drafts.csswg.org/css-sizing-3/#intrinsic-sizes
    async intrinsicSizes(children, edges, styleMap) {}

    // finally function to perform a layout
    // (`children` is an array of `.video-container` elements in our case)
    async layout(children, edges, constraints, styleMap) {

    }
  }
);
Enter fullscreen mode Exit fullscreen mode

⬆️ The API is complex, but to reach the goal we can just focus on layout function. This is where we have to write the code for sizing and positioning video elements. The browser will call this function whenever it needs to perform the layout.


async layout(children, edges, constraints, styleMap) {
  const containerWidth = constraints.fixedInlineSize; // width of a `#gallery`. Equals to the width of the screen in our case.
  const containerHeight = constraints.fixedBlockSize; // height of a `#gallery`. Equals to the height of the screen in our case.
  const videosCount = children.length;
  const aspectRatio = 16 / 9; // hardcode this for now. will improve later
Enter fullscreen mode Exit fullscreen mode

If you followed the original post, you may notice we have the same input parameters as we had in the original solution. So we can reuse the layout algorithm from the original post to calculate the gallery layout.

async layout(children, edges, constraints, styleMap) {
  const containerWidth = constraints.fixedInlineSize; // width of a `#gallery. Equals to the weight of the screen in our case.
  const containerHeight = constraints.fixedBlockSize; // height of a `#gallery`. Equals to the height of the screen in our case.
  const videosCount = children.length;
  const aspectRatio = 16 / 9; // just hardcode this for now

  // `calculateLayout` finds layout where equally sized videos with predefined aspect ratio occupy the largest area
  // see implementation in codesandbox https://codesandbox.io/s/zoom-like-gallery-with-css-houdini-0nb1m?file=/layout.js:1840-2787
  // see explanation in the original post https://dev.to/antondosov/building-a-video-gallery-just-like-in-zoom-4mam
  const { width, height, cols, rows } = calculateLayout(containerWidth, containerHeight, videosCount, aspectRatio);
  // width: fixed width for each video
  // height: fixed height for each video 
}
Enter fullscreen mode Exit fullscreen mode

Now when we have fixed width and height for all video elements, we can layout them using:

// layout video containers using calculated fixed width / height
const childFragments = await Promise.all(
  children.map(child => {
    return child.layoutNextFragment({
      fixedInlineSize: width,
      fixedBlockSize: height
     });
    })
   );
Enter fullscreen mode Exit fullscreen mode

layoutNextFragment() is part of CSS Layout API. It performs layout on child elements (.video-container in our case). It returns children as an array of LayoutFragments.

At this point all videos inside a container are laid out with sizes we calculated. The only thing left is to position them within a container (#gallery).

Positioning childFragments within the container is done by setting its inlineOffset and `block offset attributes. If not set by the author they default to zero.

A simple visualization showing positioning a LayoutFragment using inlineOffset and blockOffset in different writing modes.
image from here

`js
childFragments.forEach(childFragment => {
childFragment.inlineOffset = // assign x position for a video container
childFragment.blockOffset = // assign y position for a video container
})

return { childFragments }; // finish layout function by returning childFragments
`
Refer to codesandbox for implementation ⬆️.

On this point, everything should work, but we can make it a bit better. We hardcoded aspectRatio inside the layout code:


const aspectRatio = 16 / 9;

To make this configurable from CSS:
`js
static get inputProperties() {
return ["--aspectRatio"];
}

async layout(children, edges, constraints, styleMap) {
const containerWidth = constraints.fixedInlineSize;
const containerHeight = constraints.fixedBlockSize;
const videosCount = children.length;
// const aspectRatio = 16 / 9;
const aspectRatio = parseFloat(styleMap.get("--aspectRatio").toString());

// ...
return childFragments
}
`
And now pass it from CSS:
`
css

gallery {

height: 100%;
display: layout(zoom-like-gallery);
--aspectRatio: 1.77; /* 16 / 9 */ 👈
}
`
That's a wrap 🥇. Working solution is here. If you use Chrome, make sure you have experimental-web-platform-features flag enabled. Check support for other browsers here.
{% codesandbox zoom-like-gallery-with-css-houdini-0nb1m runonclick=1 %}

Video in case you use a browser without CSS Layout API support:

{% vimeo 426310990 %}

How is it different from the original solution?

Both implementations use the same algorithm to calculate the layout for the #gallery.

Nevertheless, there are a couple of notable differences:

  1. When #gallery layout is recalculated.
  2. What triggers the recalculation.
  3. How #gallery layout values propagate to the screen.

In the original implementation, we added a debounced event listener to the resize event on a window. We recalculated the gallery layout on a main thread whenever an event fired. Then we changed CSS using calculated layout values and this triggered the browser rendering engine to re-layout videos for new screen dimensions.


resize event -> recalculate -> change CSS -> browser performs re-layout

In the implementation with CSS Layout API, the browser rendering engine calls layout() on its own whenever it decides it needs to recalculate the layout for #gallery. We didn't have to listen for resizes and didn't have to manually manipulate DOM. Our code to calculate layout for the #gallery is being executed as part of a browser rendering engine process. Browser may even decide to execute it in a separate thread leaving less work to perform on the main thread, and our UI may become more stable and performant 🎉.

Conclusion

Unfortunately, we can't deploy this to production just yet (support). Have to leave original implementation for now. But the future is exciting! Developers soon will have an API to hook into browser rendering engine making their apps more stable and performant!

Learn more

Top comments (0)