Responsive CSS Grid with persistent aspect ratio - html

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)

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:
Set the column widths as you suggested:
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr))
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)

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

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>

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>

Related

How to insert div at the end of a grid?

I have the following code:
.my-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-gap: 20px;
}
.item {
background-color: #ddd;
height: 300px;
}
<div class="my-grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
<div class="new-item">hello!</div>
Which looks like this in my testing:
Notice how the hello is on the next row.
Is there any way to instead have it inserted after the grid above? Such that it is the last grid item? Such that it is in line with it, instead of being on the next row.
The obvious solution is to just place the new-item div as a child of my-grid, but I cannot do that in my case, because my-grid is an external component from the internet, so I cannot insert something inside of it.
These two elements also have a common parent as well. Which can be anything.
You can consider display: contents if they share the same container:
/* transfer all the styles to container
in the near future you can do .container:has(.my-grid) {}
*/
.container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-gap: 20px;
}
.item {
background-color: #ddd;
height: 300px;
}
/* we remove "my-grid" div*/
.my-grid {
display: contents;
}
<div class="container">
<div class="my-grid">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
<div class="new-item">hello!</div>
</div>
You can set absolute position on "new-item" and if you have a div container above "my-grid" do not forget to set "position: relative", on it. Then you can adjust the left and top of "new-item", relative to the main div container. Might course some problem in different resolutions of the screen, adress it with some media queries.

Fix/Improve Responsive CSS Grid

I'm building an app that needs a grid system, here's how far I've come:
I have a problem:
Each of the blocks should fill up the entire space of the grid
As you can see, the black block is not occupying the rest of the space. I want the boxes to occupy the rest white space(but all their widths should be the same).
I've read Expand a div to fill the remaining width and Make last element take remaining width with wrapping (and with IE9 support), but they don't answer the case of the dynamically wrapping grid that is here the important part since they are made for float and display: block.
Now let's look at the code:
HTML:
<div className='container'>
<div className="item"></div>
<div className="item"></div>
<div className="item"></div>
<div className="item"></div>
<div className="item"></div>
<div className="item"></div>
<div className="item"></div>
</div>
CSS:
container {
overflow: hidden;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.item {
height: 300px;
min-width: 300px;
background-color: red;
border: 2px solid yellow;
}
.container is the grid parent and .items are the children. Keep in mind that the solution should work with any number of children(.item). Thank you in advance!
I think with plain css this is not possible using grid. You could use flexbox with flex-grow instead of grid but when two items wrap in the next line they both are expanded.
Working example (a bit smaller then your example):
I don't know what that className attribute in your divs is for, i presume you meant just "class" and used that, respectively an id in the container for simplicity.
#container {
overflow: hidden;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.item {
height: 120px;
min-width: 120px;
background-color: red;
border: 2px solid yellow;
flex-grow: 1;
}
#last {
flex-grow: 2;
}
<div id='container'>
<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" id="last"></div>
</div>
If it is important to use grid it can be done with a small javascript function. Find out how many columns the grid has and in which column the item starts and set its gridColumnStart and gridColumnEnd so that it uses all the remaining grid areas. For beeing responsive you have to add an event listener that reacts on resize and calls the expand function.
Working example:
const container = document.querySelector('#container');
const last_item = document.querySelector('#last');
function expandLastItem() {
const container_style = window.getComputedStyle(container);
const item_style = window.getComputedStyle(last_item);
const offset_left = last_item.offsetLeft;
const grid_width = parseInt(container_style.width);
const item_width = parseInt(item_style.width);
const row_length = Math.floor(grid_width / item_width);
const grid_column = Math.floor(offset_left / item_width) + 1;
last_item.style.gridColumnStart = grid_column;
last_item.style.gridColumnEnd = row_length + 1;
}
window.addEventListener('resize', function() {
last_item.style.gridColumnStart = '';
last_item.style.gridColumnEnd = '';
expandLastItem();
});
expandLastItem();
#container {
overflow: hidden;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.item {
height: 120px;
min-width: 120px;
background-color: red;
border: 2px solid yellow;
}
<div id='container'>
<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" id="last"></div>
</div>
If you are using jQuery the code gets a bit simpler.
Working example:
function expandLastItem() {
const offset_left = $('#last').offset().left;
const grid_width = parseInt($('#container').width());
const item_width = parseInt($('#last').width());
const row_length = Math.floor(grid_width / item_width);
const grid_column = Math.floor(offset_left / item_width) + 1;
$('#last')[0].style.gridColumnStart = grid_column;
$('#last')[0].style.gridColumnEnd = row_length + 1;
}
$(window).on('resize', function() {
$('#last')[0].style.gridColumnStart = '';
$('#last')[0].style.gridColumnEnd = '';
expandLastItem();
});
expandLastItem();
#container {
overflow: hidden;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.item {
height: 120px;
min-width: 120px;
background-color: red;
border: 2px solid yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='container'>
<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" id="last"></div>
</div>

CSS grid not affected by padding-bottom

I'm using CSS grid to position a dynamic number of elements into rows using grid-auto-rows.
However the last element is always placed against the bottom of the page irrespective of any padding applied to the grid container (padding is applied to the top, left & right but not to bottom).
Does anyone know where I'm going wrong?
https://codepen.io/wrgt1/pen/XWjEwXX
#container {
position: relative;
width: 100%;
}
.grid {
width: 100%;
display: grid;
grid-auto-flow: row;
grid-auto-rows: 25%;
grid-template-columns: 1fr 1fr;
padding: 50px;
}
.item {
width: 250px;
height: 250px;
background: red;
}
<div id="container">
<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>
</div>
acutally, (just like #Shristi mentioned) the problem happens because the grid-container does not reach the bottom of the items. this happens because of the grid-auto-rows which is set to 25%. After the fourth row this fills the 100%, everything else just moves out of the container. So you should change your code as follows:
.grid {
// box-sizing: border-box;
width: 100%;
display: grid;
grid-auto-flow: row;
grid-auto-rows: 250px; // change this!
grid-template-columns: 1fr 1fr;
grid-gap: 50px; // use this for the spacing in between
padding: 50px; // use this for the spacing top, bottom, left and right of the grid
}
the working example: here
This is because the height of your grid is less than it's contents. Add height to your grid and margin-bottom instead of padding. Hope this might help
.grid {
width: 100%;
display: grid;
grid-auto-flow: row;
grid-auto-rows: 25%;
grid-template-columns: 1fr 1fr;
padding: 50px;
height: 100%;
margin-bottom: 50px;
overflow-y: scroll;
}
Sometimes padding does not work in HTML so in that case, you can either make div or use the br tag.
<div class="spacing" style="padding-bottom: 100px;">
// or do br where you want it.
<br>

How to create a variable width squares using css grid layout?

Is there a way to have grid cells whose width is based on fr units that dynamically adjust their height so that they stay square?
Also, I was hoping to do this without JS
The below fiddle has some example code. The divs with the class 'sqaure' are the ones which I want to dynamically adjust their height to match their width (which is 1fr so it changes)
https://jsfiddle.net/bpk0sLvL/403/
.holder {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 4px;
row-gap: 4px;
}
.box {
background-color: #ccc;
}
.wide {
grid-column-start: 1;
grid-column-end: 3;
}
<div class="holder">
<div class="box wide">
<p>This can be any height</p>
<p>All these divs are sized based on fr, so I want to have the two square divs stay square as they dynamically resize </p>
</div>
<div class="box sqaure">
This needs to be a sqaure
</div>
<div class="box sqaure">
This needs to be a square as well
</div>
</div>
You need to:
identify the width of .square; and
ensure that the height of .square is equal to that width.
You can identify the width of .square with one line of javascript:
var squareWidth = document.getElementsByClassName('square')[0].offsetWidth;
You can ensure the height of .square is equal to that width with two lines of javascript:
var holder = document.getElementsByClassName('holder')[0];
holder.style.gridTemplateRows = 'auto ' + squareWidth + 'px';
Working Example:
function calculateSquareHeight() {
var holder = document.getElementsByClassName('holder')[0];
var squareWidth = document.getElementsByClassName('square')[0].offsetWidth;
holder.style.gridTemplateRows = 'auto ' + squareWidth + 'px';
}
window.addEventListener('load', calculateSquareHeight, false);
window.addEventListener('resize', calculateSquareHeight, false);
.holder {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 4px;
grid-row-gap: 4px;
}
.box {
background-color: #ccc;
}
.wide {
grid-column-start: 1;
grid-column-end: 3;
}
<div class="holder">
<div class="box wide">
<p>This can be any height</p>
<p>All these divs are sized based on fr, so I want to have the two square divs stay square as they dynamically resize </p>
</div>
<div class="box square">
This needs to be a square
</div>
<div class="box square">
This needs to be a square as well
</div>
</div>

Fully responsive items with CSS grid and auto-fit minmax

The use of grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)) makes it easy to build a responsive CSS grid. The container will be filled with as many elements fit into a row, without using a media query.
.container {
display: grid;
grid-gap: 5px;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
}
.item {
height: 100px;
background: #ccc;
}
<div class="container">
<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 are wider than the screen when the screen is smaller than the min-value specified in minmax(). You can fix this by adding a media query at 400px, but this only works when you know that there's no content around the container. And that's almost impossible when the container could be placed anywhere.
Is there a way or property to tell the items that they should never be wider than 100%?
Something like: Fill the container with as many 400px items as possible, but ensure that non of them gets wider than 100% of the width of the container.
CodePen Demo
You should change grid-template-columns to grid-template-columns: repeat(auto-fit, minmax(min-content, 400px)) because minmax works this way: it tries to apply max value and if fails it applies minimum. But in this you can get blank space in your grid to the right. Demo:
.container {
display: grid;
grid-gap: 5px;
grid-template-columns: repeat(auto-fit, minmax(min-content, 400px));
}
.item {
height: 100px;
background: #ccc;
}
<div class="container">
<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>
Fill the container with as many 400px items as possible, but ensure that non of them gets wider than 100% of the width of the container.
For that you can use the "max-content" property, in your example this would be:
.unresponsive {
grid-template-columns: repeat(auto-fit, minmax(400px, auto));
grid-auto-columns: max-content;
}