Responsive CSS Grid with persistent aspect ratio

后端 未结 3 1476
长发绾君心
长发绾君心 2021-02-07 18:26

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:

相关标签:
3条回答
  • 2021-02-07 18:34

    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(160p, 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)

    0 讨论(0)
  • 2021-02-07 18:36

    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>

    0 讨论(0)
  • 2021-02-07 18:46

    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>

    0 讨论(0)
提交回复
热议问题