Responsive CSS Grid with persistent aspect ratio

19,956

Solution 1

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)

Solution 2

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

Solution 3

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>

Solution 4

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>

Share:
19,956
Flo
Author by

Flo

Updated on June 10, 2022

Comments

  • Flo
    Flo about 2 years

    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)

  • Flo
    Flo almost 6 years
    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?
  • dasfdsa
    dasfdsa almost 6 years
    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?
  • Flo
    Flo almost 6 years
    Take a look at this page: link (I want a similar kind of look, but without specifying 10 different @media screen widths)
  • dasfdsa
    dasfdsa almost 6 years
    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.
  • Flo
    Flo almost 6 years
    But on the page, I sent you the items don't stretch (They scale equally on both sides)
  • Flo
    Flo almost 6 years
    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?
  • Danield
    Danield almost 6 years
    yes, try changing auto-fit to auto-fill - see my post here in point 2 i explained the difference
  • Flo
    Flo almost 6 years
    Oh how could I not thought about this... Thank you so much for your help!
  • Malachi
    Malachi over 5 years
    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?
  • romeplow
    romeplow over 4 years
    I'm curious how you would achieve this with images. Not with background images, but with images using the object-fit css property.
  • Chad Retz
    Chad Retz about 4 years
    This does not work well with a fixed height in my experience.
  • martin
    martin almost 4 years
    This is great, I've been looking all over for something like this, thank you for posting!
  • martin
    martin almost 4 years
    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?
  • vsync
    vsync over 3 years
    Not responsive, as OP requested. has fixed sizes.
  • 井上智文
    井上智文 almost 3 years
    this is fixed height 90px
  • Wirde
    Wirde almost 3 years
    THIS! Just this! Oh, you are a life-saver!