2/25/2022

Creating a flexible, fixed-grid layout

Use relative units in combination with the aspect-ratio property to create responsive, smoothly scaling fixed-grid layouts.

I ran into an issue where I needed to create a CSS grid layout with a fixed number of columns and rows (in this case, an 8 x 8 grid), with the following requirements:

The specific use case for this layout is to create a responsive chessboard that sizes based on the user's viewport. Here is the final result:

See the Pen Responsive Chessboard by John Mayes (@JohnMayes) on CodePen.

Here is the basic CSS:

          
  .grid-container {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    width: 80vmin;
    max-width: 480px;
  } 

  .grid-item {
    display: flex;
    align-items: center;
    justify-content: center;
    width: auto;
    height: auto;
    aspect-ratio: 1 / 1;
  }
          
        

Here is the TLDR:

We start with a grid container with 8 columns and 8 rows, each taking up 1fr of space. The width of the container is responsively calculated based on the width of the user's viewport. The width of each grid item is set to auto, so it will expand to fill as much space as is available to it, in this case 1fr. By using aspect-ratio, we calculate the height of the grid items based on the item's own width. Whenever the viewport changes size, the width and height of both the grid items and the grid container will expand and shrink responsively, while maintaining it's aspect ratio. By using vmin to calculate the width of the grid container, we ensure that the container will never grow larger than the viewport's smaller dimension.

And here is the long-form write up for how I got there:

The HTML

The actual code I am working with uses React with functional components to dynamically generate each square of the chessboard, along with is classes, which are then displayed as a grid. For the purposes of this demo, however, we can just use some hand-jammed HTML:

  
  <h1>Chessboard</h1>

  <div class="board">
    <div class="square white" id="1"></div>
    <div class="square black" id="2"></div>
    <div class="square white" id="3"></div>

    ...

    <div class="square white" id="64"></div>
  
        

We now have one "board" div with 64 squares as children.

The Grid Container

We are going to use grid-template-columns: repeat to create a grid layout of 8 columns (the width of a chessboard), with each column taking up 1fr of available space. The 64 divs we created earlier will then fill in our gird from left to right, top to bottom, to from a nice 8 x 8 grid. However, the grid items are currently empty, so nothing will display! Let's set the height and width of the grid items to something arbitrary, say 40 pixels, and then inspect with dev tools to see our grid in action.

Getting somewhere. However, nothing is yet responsive. Let's change that.

The Grid Items

First, we are going to change the grid item's height and width to auto. This will keep each element the same height and width as its parent container (the grid track) — which is currently nothing, so again, nothing will display. In order to see anything rendered, the grid container must have an explicit width.

In order to make the grid responsive, we can set the container's width to a relative unit of measurement. In this case I chose 60vw. Now the width of the grid container will grow and shrink responsively, relative to the width of the user's viewport.

Almost there. However, we are still lacking an explicit height, so, again, nothing is being displayed (remember that so far we are only working with empty divs). This is where aspect-ratio comes into play. The aspect-ratio CSS property directly specifies a fixed width to height aspect ratio for an element. It also does not rely on parent size or viewport size: it relies on the element's own width or height. For a perfect square (what we need in this case), use aspect-ratio: 1 / 1.

So now we have a grid container with 8 columns, each taking up 1fr of space. The width of the container is responsively calculated based on the width of the user's viewport. The width of each grid item is set to auto, so it will expand to fill as much space as is available to it, in this case 1fr, which is again responsively calculated. Finally, by using aspect-ratio, we calculate the height of the grid items based on it's own width. Now the grid items are displayed. Further, whenever the viewport changes size, the width and height of both the grid items the the grid container will expand and shrink responsively, while maintaining it's aspect ratio.

Finally, setting the grid items to display: flex will set us up for the final "piece" of this puzzle. The chess pieces, to be exact.

The SVG elements

The project I am working on uses SVGs to represent chess pieces on a board. I need those SVGs to scale along with the chessboard! However, explaining SVG scaling in detail is beyond the scope of this article. The TLDR is that in order for an SVG element to scale proportionally with the size of it's parent element, while maintaining it's aspect ratio, the width and height values of the SVG's viewBox attribute need to equal the width and height of the coordinate system used in the SVG element itself (i.e, viewBox="0 0 [original svg width] [original svg height]"). Next, remove any height and width properties on the SVG element itself — the SVG will then scale to fill the available space in it's parent element, while automatically preserving it's aspect ratio. Nice!

Bonus: a little more mobile responsiveness

Let's change the unit of measurement used to calculate our grid width from vw to vmin. This ensure that the grid container will never grow larger than the viewport's smaller dimension. This means that on a mobile device the chessboard will always be fully visible, wether or not the device is in portrait or landscape mode. No media queries required.

Again, here is the final result:

See the Pen Responsive Chessboard by John Mayes (@JohnMayes) on CodePen.