Responsive CSS Grid with persistent aspect ratio
Asked Answered
M

6

30

My goal is to create a responsive grid with an unknown amount of items, that keep their aspect ratio at 16 : 9. Right now it looks like this:

.grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, 160px);
    grid-template-rows: 1fr;
    grid-gap: 20px;
 }
.item {
    height: 90px;
    background: grey;
}
<div class="grid">
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
</div>

The problem is, that the items won't scale with the screen size, resulting in a margin at the right site. But when making the grid adapt to the screen size with e.g.: grid-template-columns: repeat(auto-fit, minmax(160p, 1fr)) and removing the height: 90px;, the aspect ratio doesn't persist.

Maybe there is a better solution without css grid? (Maybe using javascript)

Mammalogy answered 29/7, 2018 at 6:23 Comment(2)
Something like this: codepen.io/danield770/pen/bjYvOj ?Abecedarium
This is exactly what I was looking for! If you answer the question I'll tick your's as the solution :) Thank you!Mammalogy
A
20

You could take advantage of the fact that padding in percentages is based on width.

This CSS-tricks article explains the idea quite well:

...if you had an element that is 500px wide, and padding-top of 100%, the padding-top would be 500px.

Isn't that a perfect square, 500px × 500px? Yes, it is! An aspect ratio!

If we force the height of the element to zero (height: 0;) and don't have any borders. Then padding will be the only part of the box model affecting the height, and we'll have our square.

Now imagine instead of 100% top padding, we used 56.25%. That happens to be a perfect 16:9 ratio! (9 / 16 = 0.5625).

So in order for the columns to maintain aspect ratio:

  1. Set the column widths as you suggested:

    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr))
    
  2. Add a pseudo element to the items to maintain the 16:9 aspect ratio:

    .item:before {
      content: "";
      display: block;
      height: 0;
      width: 0;
      padding-bottom: calc(9/16 * 100%);
    }
    

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  grid-template-rows: 1fr;
  grid-gap: 20px;
}
.item {
  background: grey;
  display: flex;
  justify-content: center;
}
.item:before {
  content: "";
  display: block;
  height: 0;
  width: 0;
  padding-bottom: calc(9/16 * 100%);
}
<div class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

Codepen Demo (Resize to see the effect)

Abecedarium answered 29/7, 2018 at 8:33 Comment(6)
I now encountered a small issue... The items become really huge if there are only 1 or 2 items in the grid... Is there a way to prevent this behavior?Mammalogy
yes, try changing auto-fit to auto-fill - see my post here in point 2 i explained the differenceAbecedarium
Oh how could I not thought about this... Thank you so much for your help!Mammalogy
When I put in a lot of text in the first item, the whole row of items stretch at the bottom. Isn't there a way to KEEP the 16:9 ratio?Crepitate
I'm curious how you would achieve this with images. Not with background images, but with images using the object-fit css property.Statism
This does not work well with a fixed height in my experience.Endemic
O
22

CSS Evolution : aspect-ratio property

We can now use aspect-ratio CSS4 property (Can I Use ?) to manage easily aspect ratio without padding and pseudo-element tricks. Combined with object-fit we obtain very interesting rendering.

Here, photos of various ratios I need to render in 16/9 :

section {
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); /* Play with min-value */
}

img {
  background-color: gainsboro; /* To visualize empty space */
  aspect-ratio: 16/9; 
  /*
    "contain" to see full original image with eventual empty space
    "cover" to fill empty space with truncating
    "fill" to stretch
  */
  object-fit: contain;
  width: 100%;
}
<section>
  <img src="https://placeimg.com/640/360/architecture">
  <img src="https://placeimg.com/640/360/tech">
  <img src="https://placeimg.com/360/360/animals">
  <img src="https://placeimg.com/640/360/people">
  <img src="https://placeimg.com/420/180/architecture">
  <img src="https://placeimg.com/640/360/animals">
  <img src="https://placeimg.com/640/360/nature">
</section>

Playground : https://codepen.io/JCH77/pen/JjbajYZ

Oralle answered 5/3, 2021 at 21:2 Comment(0)
A
20

You could take advantage of the fact that padding in percentages is based on width.

This CSS-tricks article explains the idea quite well:

...if you had an element that is 500px wide, and padding-top of 100%, the padding-top would be 500px.

Isn't that a perfect square, 500px × 500px? Yes, it is! An aspect ratio!

If we force the height of the element to zero (height: 0;) and don't have any borders. Then padding will be the only part of the box model affecting the height, and we'll have our square.

Now imagine instead of 100% top padding, we used 56.25%. That happens to be a perfect 16:9 ratio! (9 / 16 = 0.5625).

So in order for the columns to maintain aspect ratio:

  1. Set the column widths as you suggested:

    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr))
    
  2. Add a pseudo element to the items to maintain the 16:9 aspect ratio:

    .item:before {
      content: "";
      display: block;
      height: 0;
      width: 0;
      padding-bottom: calc(9/16 * 100%);
    }
    

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  grid-template-rows: 1fr;
  grid-gap: 20px;
}
.item {
  background: grey;
  display: flex;
  justify-content: center;
}
.item:before {
  content: "";
  display: block;
  height: 0;
  width: 0;
  padding-bottom: calc(9/16 * 100%);
}
<div class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

Codepen Demo (Resize to see the effect)

Abecedarium answered 29/7, 2018 at 8:33 Comment(6)
I now encountered a small issue... The items become really huge if there are only 1 or 2 items in the grid... Is there a way to prevent this behavior?Mammalogy
yes, try changing auto-fit to auto-fill - see my post here in point 2 i explained the differenceAbecedarium
Oh how could I not thought about this... Thank you so much for your help!Mammalogy
When I put in a lot of text in the first item, the whole row of items stretch at the bottom. Isn't there a way to KEEP the 16:9 ratio?Crepitate
I'm curious how you would achieve this with images. Not with background images, but with images using the object-fit css property.Statism
This does not work well with a fixed height in my experience.Endemic
E
6

I needed this exact same thing for video layouts, but I couldn't use the other answers because I need to be bounded by width and height. Basically my use case was a container of a certain size, unknown item count, and a fixed aspect ratio of the items. Unfortunately this cannot be done in pure CSS, it needs some JS. I could not find a good bin packing algorithm so I wrote one myself (granted it might mimic existing ones).

Basically what I did is took a max set of rows and found the fit with the best ratio. Then, I found the best item bounds retaining the aspect ratio, and then set that as auto-fit height and width for the CSS grid. The result is quite nice.

Here's a full example showing how to use it with something like CSS custom properties. The first JS function is the main one that does the work of figuring out the best size. Add and remove items, resize browser to watch it reset to best use space (or you can see this CodePen version).

// Get the best item bounds to fit in the container. Param object must have
// width, height, itemCount, aspectRatio, maxRows, and minGap. The itemCount
// must be greater than 0. Result is single object with rowCount, colCount,
// itemWidth, and itemHeight.
function getBestItemBounds(config) {
  const actualRatio = config.width / config.height
  // Just make up theoretical sizes, we just care about ratio
  const theoreticalHeight = 100
  const theoreticalWidth = theoreticalHeight * config.aspectRatio
  // Go over each row count find the row and col count with the closest
  // ratio.
  let best
  for (let rowCount = 1; rowCount <= config.maxRows; rowCount++) {
    // Row count can't be higher than item count
    if (rowCount > config.itemCount) continue
    const colCount = Math.ceil(config.itemCount / rowCount)
    // Get the width/height ratio
    const ratio = (theoreticalWidth * colCount) / (theoreticalHeight * rowCount)
    if (!best || Math.abs(ratio - actualRatio) < Math.abs(best.ratio - actualRatio)) {
      best = { rowCount, colCount, ratio }
    }
  }
  // Build item height and width. If the best ratio is less than the actual ratio,
  // it's the height that determines the width, otherwise vice versa.
  const result = { rowCount: best.rowCount, colCount: best.colCount }
  if (best.ratio < actualRatio) {
    result.itemHeight = (config.height - (config.minGap * best.rowCount)) / best.rowCount
    result.itemWidth = result.itemHeight * config.aspectRatio
  } else {
    result.itemWidth = (config.width - (config.minGap * best.colCount)) / best.colCount
    result.itemHeight = result.itemWidth / config.aspectRatio
  }
  return result
}

// Change the item size via CSS property
function resetContainerItems() {
  const itemCount = document.querySelectorAll('.item').length
  if (!itemCount) return
  const container = document.getElementById('container')
  const rect = container.getBoundingClientRect()
  // Get best item bounds and apply property
  const { itemWidth, itemHeight } = getBestItemBounds({
    width: rect.width,
    height: rect.height,
    itemCount,
    aspectRatio: 16 / 9,
    maxRows: 5,
    minGap: 5
  })
  console.log('Item changes', itemWidth, itemHeight)
  container.style.setProperty('--item-width', itemWidth + 'px')
  container.style.setProperty('--item-height', itemHeight + 'px')
}

// Element resize support
const resObs = new ResizeObserver(() => resetContainerItems())
resObs.observe(document.getElementById('container'))

// Add item support
let counter = 0
document.getElementById('add').onclick = () => {
  const elem = document.createElement('div')
  elem.className = 'item'
  const button = document.createElement('button')
  button.innerText = 'Delete Item #' + (++counter)
  button.onclick = () => {
    document.getElementById('container').removeChild(elem)
    resetContainerItems()
  }
  elem.appendChild(button)
  document.getElementById('container').appendChild(elem)
  resetContainerItems()
}
#container {
  display: inline-grid;
  grid-template-columns: repeat(auto-fit, var(--item-width));
  grid-template-rows: repeat(auto-fit, var(--item-height));
  place-content: space-evenly;
  width: 90vw;
  height: 90vh;
  background-color: green;
}

.item {
  background-color: blue;
  display: flex;
  align-items: center;
  justify-content: center;
}
<!--
Demonstrates how to use CSS grid and add a dynamic number of
items to a container and have the space best used while
preserving a desired aspect ratio.

Add/remove items and resize browser to see what happens.
-->
<button id="add">Add Item</button><br />
<div id="container"></div>
Endemic answered 2/5, 2020 at 6:17 Comment(4)
This is great, I've been looking all over for something like this, thank you for posting!Boykins
If you have 2 divs in the first row, and 1 div in the second row, have you found a way to center the 1 div on the second row?Boykins
This is pretty much exactly what I want except there seem to be some container sizes for which the elements will not fill the space for some reason. i.imgur.com/BQ7Do6A I am not able to figure out why...Leonleona
Here is a video which shows the strange behavior imgur.com/a/516kCOcLeonleona
B
0

Maybe I am not able to understand the question, but how do you plan to keep item aspect ratio 16:9, while wanting to scale items with screen width?

If you are fine with items having enlarged width in between screen sizes, this can work:

.grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    grid-template-rows: 1fr;
    grid-gap: 20px;
 }
.item {
    height: 90px;
    background: grey;
}
<div class="grid">
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
</div>
Beckwith answered 29/7, 2018 at 7:54 Comment(7)
Your solution is indeed a good idea, but the problem now is, that the 16 : 9 ratio isn't preserved. They shouldn't "stretch" along the x-axis. Is there somehow a way to scale them at both axes?Mammalogy
When the screen width enlargens, they will either stretch (which will destroy 16:9, my solution) OR they will leave margins (in between or in the end, your solution). There is no third possibility, imo. Which one do you want?Beckwith
Take a look at this page: link (I want a similar kind of look, but without specifying 10 different @media screen widths)Mammalogy
My solution does exactly that. Try it here. If you incease width of screen, items will temporarily strech but when screen gets so wide that a new item can fit in, all item will be back to 160px width and a new item (160px) will join in.Beckwith
But on the page, I sent you the items don't stretch (They scale equally on both sides)Mammalogy
Not responsive, as OP requested. has fixed sizes.Torietorii
this is fixed height 90pxSemination
L
0

Chad Retz's answer is almost exactly what I wanted, however it seems to have some odd behavior.

With the help of a developer on fiverr I modified it a bit and it seems to work better. I am sure there is probably room for some optimization if anyone wants to take a stab at it.

// Get the best item bounds to fit in the container. Param object must have
// width, height, itemCount, aspectRatio, maxRows, and minGap. The itemCount
// must be greater than 0. Result is single object with rowCount, colCount,
// itemWidth, and itemHeight.
function getBestItemBounds(config) {
  const actualRatio = config.width / config.height
  console.log(actualRatio)
  
  // Go over each row count find the row and col count with the closest ratio.
  let best
  console.log(config.aspectRatio)
  for (let rowCount = 1; rowCount <= config.maxRows; rowCount++) {
    // Row count can't be higher than item count
    if (rowCount > config.itemCount) continue
    
    // calc column count
    const colCount = Math.ceil(config.itemCount / rowCount)

    // Get the width/height ratio of container for this row,col count
    const ratio = (config.aspectRatio * colCount) / rowCount

    // calc actual item width, height for this row,col layout
    let itemHeight;
    let itemWidth;
    // If the best ratio is less than the actual ratio, it's the height that determines the width, otherwise vice versa.
    if (ratio < actualRatio) {
     itemHeight = (config.height - (config.minGap * rowCount)) / rowCount
     itemWidth = itemHeight * config.aspectRatio
    } else {
     itemWidth = (config.width - (config.minGap * colCount)) / colCount
     itemHeight = itemWidth / config.aspectRatio
    }

    // calc total area of all elements
    var totalItemArea = config.itemCount * itemHeight * itemWidth
    // we want to maximize usage of container area, so minimize difference between container area and total area of all items
    var diff = (config.width * config.height) - totalItemArea
    if (!best || diff < best.diff) {
      best = { itemWidth, itemHeight, ratio, diff }
    }
  }
  
  const result = { itemWidth: best.itemWidth, itemHeight: best.itemHeight }

  console.log(result)
  return result
}

// Change the item size via CSS property
function resetContainerItems() {
  const itemCount = document.querySelectorAll('.item').length
  if (!itemCount) return
  const container = document.getElementById('container')
  const rect = container.getBoundingClientRect()
  // Get best item bounds and apply property
  const { rowCount, colCount, itemWidth, itemHeight } = getBestItemBounds({
    width: rect.width,
    height: rect.height,
    itemCount,
    aspectRatio: 16 / 9,
    maxRows: 10,
    minGap: 5
  })
  container.style.setProperty('--item-width', itemWidth + 'px')
  container.style.setProperty('--item-height', itemHeight + 'px')
}

// Element resize support
const resObs = new ResizeObserver(() => resetContainerItems())
resObs.observe(document.getElementById('container'))

// Add item support
let counter = 0
document.getElementById('add').onclick = () => {
  const elem = document.createElement('div')
  elem.className = 'item'
  const button = document.createElement('button')
  button.innerText = 'Delete Item #' + (++counter)
  button.onclick = () => {
    document.getElementById('container').removeChild(elem)
    resetContainerItems()
  }
  elem.appendChild(button)
  document.getElementById('container').appendChild(elem)
  resetContainerItems()
}
#container {
  display: inline-grid;
  grid-template-columns: repeat(auto-fit, var(--item-width));
  grid-template-rows: repeat(auto-fit, var(--item-height));
  place-content: space-evenly;
  width: 90vw;
  height: 90vh;
  background-color: green;
}

.item {
  background-color: blue;
  display: flex;
  align-items: center;
  justify-content: center;
}
<!--
Demonstrates how to use CSS grid and add a dynamic number of
items to a container and have the space best used while
preserving a desired aspect ratio.

Add/remove items and resize browser to see what happens.
-->
<button id="add">Add Item</button><br />
<div id="container"></div>
Leonleona answered 19/11, 2023 at 23:56 Comment(0)
A
0

For what I needed, was perfect squares, based on the selected answer by Stephen Ostermiller but without the need of the ::before pseudo element

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  grid-gap: 20px;
}

.item {
  background: grey;
  display: flex;
  justify-content: center;
  aspect-ratio: 1 / 1;
}
<div class="grid">
  <div class="item"></div>
  <div class="item"></div>
</div>
Axletree answered 27/1 at 1:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.