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
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>
And making sure #gallery
takes up the whole screen:
body {
margin: 0;
height: 100vh;
}
#gallery {
height: 100%;
}
display: layout(zoom-like-gallery)
This is the moment where Houdini 🎩 does his magic:
#gallery {
height: 100%;
display: layout(zoom-like-gallery); // 💥
}
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");
}
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) {
}
}
);
⬆️ 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
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
}
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
});
})
);
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.
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
}
`
css
And now pass it from 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:
- When
#gallery
layout is recalculated. - What triggers the recalculation.
- 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
- Practical overview of CSS Houdini
- Houdini: Demystifying CSS
- ishoudinireadyyet.com
- CSS Layout API examples
- CSS Layout API spec
- I skipped the concept of Worklets trying to keep this hands-on post simpler.
Top comments (0)