具有持久宽高比的响应式CSS网格

28
我的目标是创建一个响应式网格,其中包含未知数量的项目,并保持它们的纵横比为16:9。
目前看起来是这样的:

.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>

问题在于,项目不会随着屏幕大小而缩放,导致右侧出现边距。但是,通过使网格适应屏幕大小,例如:grid-template-columns: repeat(auto-fit, minmax(160p, 1fr)) 并移除 height: 90px;,纵横比例将无法保持。

也许有一种更好的解决方案可以不使用 CSS Grid?(可能使用 JavaScript)


类似这样的东西:https://codepen.io/danield770/pen/bjYvOj? - Danield
这正是我在寻找的!如果您回答了我的问题,我会将您的答案标记为解决方案 :) 谢谢! - Florian Ludewig
5个回答

20

你可以利用百分比填充是基于宽度的事实。

这篇CSS技巧文章很好地解释了这个想法:

…如果您有一个宽度为500px的元素,并且填充顶部为100%, 则填充顶部将为500px。

那不是一个完美的正方形,500px×500px吗? 是的,是的! 宽高比!

如果我们强制将元素的高度设为零(height: 0;),并且没有任何边框。 那么填充将是影响高度的盒模型的唯一部分,我们将拥有一个正方形。

现在想象一下,我们使用了56.25%的顶部填充而不是100%。 那恰好是完美的16:9比例! (9 / 16 = 0.5625)。

因此,为了使列保持宽高比:

  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演示(调整大小以查看效果)


我现在遇到了一个小问题...如果网格中只有1或2个项目,它们会变得非常巨大...有没有办法防止这种行为? - Florian Ludewig
1
是的,请尝试将“auto-fit”更改为“auto-fill”-请参阅我的帖子此处,在第2点中我解释了它们之间的区别。 - Danield
1
哦,我怎么没想到这个...非常感谢你的帮助! - Florian Ludewig
当我在第一个项目中输入大量文本时,整行项目会在底部拉伸。难道没有一种方法可以保持16:9的比例吗? - Malachi
我很好奇您如何使用图像来实现这一点。不是使用背景图像,而是使用object-fit CSS属性的图像。 - romeplow
根据我的经验,这在固定高度上效果不佳。 - Chad Retz

19

CSS进化:aspect-ratio属性

现在我们可以使用CSS4的aspect-ratio属性(Can I Use ?)轻松管理宽高比,无需使用填充和伪元素技巧。结合object-fit,我们可以得到非常有趣的渲染效果。

这里展示了各种宽高比的照片,我们需要呈现为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>

代码测试场 : https://codepen.io/JCH77/pen/JjbajYZ


5

我需要在视频布局中使用这个东西,但是我不能使用其他答案,因为我需要受到宽度 高度的限制。基本上我的用例是一个特定大小的容器、未知项目数量和固定项纵横比。不幸的是,这不能在纯CSS中完成,需要一些JS。我找不到一个好的装箱算法,所以我自己写了一个(虽然可能模仿了现有的算法)。

基本上我做的是取一组最大行,并找到最佳比例的适配项。然后,我找到了保持项纵横比的最佳项边界,然后将其设置为CSS网格的自适应高度和宽度。结果非常好。

这里有一个完整的示例,展示如何将其与类似CSS自定义属性的东西结合使用。第一个JS函数是主要函数,负责找出最佳的大小。添加和删除项目,调整浏览器大小以查看其重置以最佳利用空间(或者您可以查看此CodePen版本)。

// 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>


这太棒了,我一直在寻找这样的东西,谢谢你的发布! - martin
如果您在第一行有2个div,并且在第二行有1个div,您是否找到了将第二行的1个div居中的方法? - martin
这基本上就是我想要的,只是有一些容器尺寸,元素似乎无法填满空间,原因不明。i.imgur.com/BQ7Do6A 我无法弄清楚为什么... - undefined
这里有一个视频展示了奇怪的行为imgur.com/a/516kCOc - undefined

0
也许我无法理解这个问题,但是您打算如何在希望根据屏幕宽度缩放项目的同时保持项目纵横比16:9?
如果您可以接受在屏幕尺寸之间具有扩大宽度的项目,则可以使用以下方法:

.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>


2
你的解决方案确实是一个好主意,但现在的问题是16:9的比例没有被保留。它们不应该沿着x轴“拉伸”。有没有办法在两个轴上同时缩放它们? - Florian Ludewig
当屏幕宽度变大时,它们要么会被拉伸(这将破坏16:9,我的解决方案),要么会留下边距(在中间或末尾,你的解决方案)。在我看来,没有第三种可能性。你想选哪一个? - dasfdsa
但是在我发送给你的页面上,这些项目不会拉伸(它们在两侧等比例缩放)。 - Florian Ludewig
不具有响应性,如OP所请求的那样,具有固定大小。 - vsync
这是固定高度为90像素。 - 井上智文
显示剩余2条评论

0
Chad Retz的回答几乎是我想要的,但是它似乎有一些奇怪的行为
在一个fiverr上的开发者的帮助下,我对它进行了一些修改,现在它似乎工作得更好了。 如果有人想要尝试优化一下,我相信可能还有一些空间。

// 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>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接