No Time Dad

A blog about web development written by a busy dad

Grid Square Links Example

Finding the element or pages to re-create has become a problem for me lately. I’m having a hard time jumping into projects that remain small. Maybe I need to consider breaking them into smaller projects or focusing on a smaller piece of the larger project.

This example was one of those larger projects. There were a ton of other elements I was going to try to build and include, but in the end I decided it would’ve been a huge amount of work and difficult to write about, or even remember what I was doing.

I didn’t really know what to call this example or how to describe it. It’s basically four squares that are each a link to a page somewhere in an application. Each square has it’s own svg image, but they all share the same styles.

All four link squares are inside of a grid. On small screens, the grid has a single column. On larger screens, the grid changes to a two column grid. The change is done by adding a media query and updating the number of columns in the corresponding selector.

Live example of the responsive css grid square links. Try resizing the browser window to see how the grid responds. This example was written in react, mostly for convenience. It can be translated to other web frameworks as needed.

Final code is available at the bottom of this page.

Building the element

This example was written in react, mostly for my own convenience. Also, I really like react and I feel most productive using it. The component can be translated to other web frameworks as needed since it doesn’t use state or any other react specific things.

The other thing worth noting is that I built this element with a “mobile first” approach. Which basically means that I opened Chrome dev tools (F12), clicked on the device toolbar, set my screen size to around 320px wide, then began creating the element.

Wrapper and grid container

I thought of this element as an entire section of a page when I was building it. Most likely, that page would be for a web application. Since it’s a section of a page, it’s important to think about the elements that are potentially going to be above and beneath it. It’s likely that I’ll want the margins of this new section to match the margins of the rest of the page.

With that in mind, I decided to wrap the div element where I define the grid in a parent div where I set the margins for the entire element. This makes it easier to adjust the margins without having to worry as much about how the grid itself might change. Below is the basic structure of the element.

// index.jsx
import React from 'react';
import * as styles from './index.module.css';

const Index = () => (
  <div className={styles.wrapper}>
    <div className={styles.grid_container}>
      <a href="#home" className={styles.square}>
        ...
      </a>
      ...
    </div>
  </div>
);
/* index.module.css */
.wrapper {
  /* The max-width is small here because it's a mobile first design. 
     It'll change later for large screens */
  max-width: 300px;
  /* Center the element on the page */
  margin-left: auto;
  margin-right: auto;
}

.grid_container {
  /* Initialize the grid */
  display: grid;
  /* Add some spacing to better fit the elements */
  gap: 10px;
  padding: 1rem;
}

.square {
  ...
}

Something worth noting here is that I didn’t use grid-template-columns with auto-fit to try to make the grid responsive automatically. This is certainly one of a few different approaches I could’ve taken, but I chose not to because I wanted more fine grained control over the width at which columns are added. I also think that I would’ve had to add a media query anyways to adjust the width.

Sometimes with auto-fit you can get into a situation where you have empty spaces depending on the size of the screen. There are some cases where this isn’t a big deal to me, but in this case I wanted to ensure that I didn’t have any extra space. Again, this is mostly just personal preference and I probably could’ve got it working with auto-fit eventually.

Squares

The square itself is made up of a parent div with two child divs. The first child div contains an svg image, and the second contains description text and an svg arrow icon. I thought the square would look best with the text and svg arrow icon pushed all the down to the bottom of the square.

There are a few different ways to push elements down, including adding margin and padding. These are both decent approaches, but they have an element of randomness to them. Another approach that I find much more consistent is to use auto margins inside of a flexbox container to push elements as far left, right, up, or down as possible inside the container.

Where I create the flexbox container is important. Each section of the square element, including the parent element, will be a flexbox container. And each container could have a different flex-direction depending on how I want the contents to align.

It’s easy to get lost in flexbox, especially if you have multiple containers. This is why I think it’s important to break elements into sections. Zooming out and looking at the description text and arrow icon as a single element instead of two elements, I can easily see where to apply my auto margin.

I think “zooming out” on multiple elements and trying to see them as a single elements is sometimes helpful for through alignment issues. Especially when it comes to flexbox.

// index.jsx
...
import investing from './investing.svg';

...
<a href="#home" className={styles.square}>
  <div className={styles.square__header}>
    <img className={styles.svg_img} src={investing} alt="investing" />
  </div>
  <div className={styles.square__content}>
    <div className={styles.text}>Investing</div>
    <ArrowNarrowRight />
  </div>
</a>
...
/* index.module.css */
...
/* Parent selector for the square. */
.square {
  height: 300px;
  padding: 1rem;
  text-decoration: none;
  background-color: darkslateblue;
  color: white;
  /* Initialize flexbox to allow auto margin child elements */
  display: flex;
  flex-direction: column;
}

.square:hover {
  /* Visually show the user which element they're hovered over */
  box-shadow: 0 0.4rem 1rem rgba(0, 0, 0, 0.35);
}

.square__header {
  /* Use flexbox to vertically center the svg image in the square */
  display: flex;
  flex-direction: column;
  align-items: center;
}

.square__content {
  /* Use flexbox to help align the text and icon  */
  display: flex;
  align-items: center;
  /* Since there are only two elements, put one left and one right */
  justify-content: space-between;
  /* Push the text description and arrow icon to the bottom */
  margin-top: auto;
}

.text {
  font-weight: 600;
}

.svg_img {
  width: 220px;
  height: 220px;
}

.svg_icon {
  height: 1.5rem;
  width: 1.5rem;
}
...
Media query

This example only has one media query, but it could easily be expanded to have more. It really depends on how the squares look on different sized screens. I think they look good as a single column until the screen size reaches 768 pixels wide, and continute to look good past that width too.

With that in mind, I only added one media query. That single media query is a min-width media query because I used a “mobile first” design approach, targeting the smallest screen size first and working my way out from there.

The media query makes changes to the .wrapper selector to increase the allowed width slightly as the screen size increases. Again, this value should be updated relative to the rest of the elements on the page. If they’re using a max-width of 900 pixels, then it might be a good idea for this element to do the same.

The media query also targets the .grid_container selector. Which is arguably the most important change in the media query. It adds the second column to the grid for screens 768 pixels or larger, creating a two by two responsive grid.

/* index.module.css */
...
@media (min-width: 768px) {
  .wrapper {
    /* Allow more space on larger screens */
    max-width: 650px;
  }

  .grid_container {
    /* Add a second column on larger screens */
    grid-template-columns: 1fr 1fr;
  }
}

Conclusion

Overall, the element is simple. But there was a lot for me to think about and work through. I think an important part of becoming a better developer is becoming better at decomposing problems and being able to zoom out on complex issues. Being able to do this allows you to keep making progress, even if it’s small.

The arrow icon used in this example is from heroicons and the svg images are from undraw.

Live example of the responsive css grid square links. Try resizing the browser window to see how the grid responds.

Final code
// package.json
...
"react": "^17.0.2"
...
// index.jsx
import React from 'react';
import * as styles from './index.module.css';
import investing from './investing.svg';
import hello from './hello.svg';
import security from './security.svg';
import lightbulb from './lightbulb.svg';

const Index = () => (
  <div className={styles.wrapper}>
    <div className={styles.grid_container}>
      <a href="#home" className={styles.square}>
        <div className={styles.square__header}>
          <img className={styles.svg_img} src={investing} alt="investing" />
        </div>
        <div className={styles.square__content}>
          <div className={styles.text}>Investing</div>
          <ArrowNarrowRight />
        </div>
      </a>
      <a href="#home" className={styles.square}>
        <div className={styles.square__header}>
          <img className={styles.svg_img} src={hello} alt="hello" />
        </div>
        <div className={styles.square__content}>
          <div className={styles.text}>Technology</div>
          <ArrowNarrowRight />
        </div>
      </a>
      <a href="#home" className={styles.square}>
        <div className={styles.square__header}>
          <img className={styles.svg_img} src={security} alt="security" />
        </div>
        <div className={styles.square__content}>
          <div className={styles.text}>Security</div>
          <ArrowNarrowRight />
        </div>
      </a>
      <a href="#home" className={styles.square}>
        <div className={styles.square__header}>
          <img className={styles.svg_img} src={lightbulb} alt="lightbulb" />
        </div>
        <div className={styles.square__content}>
          <div className={styles.text}>Ideas</div>
          <ArrowNarrowRight />
        </div>
      </a>
    </div>
  </div>
);

const ArrowNarrowRight = () => (
  <svg xmlns="http://www.w3.org/2000/svg" className={styles.svg_icon} fill="none" viewBox="0 0 24 24" stroke="currentColor">
    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
  </svg>
);

export default Index;
/* index.module.css */
.wrapper {
  max-width: 300px;
  margin-left: auto;
  margin-right: auto;
}

.grid_container {
  display: grid;
  gap: 10px;
  padding: 1rem;
}

.square {
  height: 300px;
  padding: 1rem;
  text-decoration: none;
  color: white;
  display: flex;
  flex-direction: column;
  background-color: darkslateblue;
}

.square:hover {
  box-shadow: 0 0.4rem 1rem rgba(0, 0, 0, 0.35);
}

.square__header {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.square__content {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: auto;
}

.text {
  font-weight: 600;
}

.svg_img {
  width: 220px;
  height: 220px;
}

.svg_icon {
  height: 1.5rem;
  width: 1.5rem;
}

@media (min-width: 768px) {
  .wrapper {
    max-width: 650px;
  }

  .grid_container {
    grid-template-columns: 1fr 1fr;
  }
}