Absolute positioning with CSS grid

A dark purple circle overlapping a lighter glowing circle with a halo of colors on a dark background.

This is just your friendly neighborhood reminder that you can use CSS grid to overlap elements instead of position:absolute. This has always been a feature of grid (and written about thoroughly!), but I rarely see it used in real life websites. Why is that? Grid was available to use in all major browsers by 2017, so it’s not new. My own excuse for not adopting new patterns and technologies is that old habits die hard.

Rethinking our techniques, however, is sometimes less overwhelming when we scope it to something small.

A real life transformation

We recently added the ability to insert captions under our looping video component on thoughtbot.com. The video component also includes a pause/play button that appears in the bottom right of the video. That button was absolutely positioned relative to a parent element that wrapped both that element and the video.

<figure>
  <button>Play/pause</button>
  <video autoplay muted loop playsinline src=”/”>
  <figcaption>A caption goes here.</figcaption>
</figure>
figure {
  position: relative;
}

button {
  bottom: 0;
  position: absolute;
  right: 0;
  z-index: 999;
}

video {
  display: block;
  width: 100%;
  height: auto;
}

This code without the figcaption works perfectly! However, after inserting this new element with the same CSS, our pause/play button overlaps the caption and not the video.

A mockup of a cellphone screen against a light gray background. The screen has a news article on it. Below is a caption that reads the tip of the day module allows the user to engage with a summary before reading an entire article. The caption is partially blocked by a white square with a pause icon.
The button is positioned relative to the figure container, which the figcaption is also wrapped in, so the bottom right corner is now where the text is displayed, instead of the video element.

This can be remedied a few different ways, including adjusting the HTML nesting so we can retain the absolute positioning. In this case, the quickest solution appeared to be a minor adjustment to the CSS grid.

figure {
- position: relative;
+ display: grid;
}

button {
- bottom: 0;
- position: absolute;
- right: 0;
+ grid-area: 1 / 1;
  z-index: 999;
}

video {
  display: block;
+ grid-area: 1 / 1;
  width: 100%;
  height: auto;
}

By declaring the figure as display: grid; and setting the same grid-area for both the video and button element, we’ve instructed both items to sit in the same spot on our grid. The 1 / 1 part in this example is shorthand for grid-column-start: 1 and grid-row-start: 1, placing an item on the first column and row line. This alone gets us most of the way there, but by default, the button will take up the entire width and height that the video occupies.

A large white square with a black border and a pause icon in the middle of it. Below is a caption that reads the tip of the day module allows the user to engage with a summary before reading an entire article.
The play button is in the correct spot but is much too big and blocks the video.

Adding place-self: end; to the button element puts it in the bottom right corner, just where we want it! place-self is shorthand for align-self: end; (which puts the element flush to the bottom of our container) and justify-self: end; (which puts the element flush to the right of our container).

button {
  grid-area: 1 / 1;
+ place-self: end;
  z-index: 999;
}

A mockup of a cellphone screen against a light gray background with a white pause button in the bottom right corner. The screen has a news article on it. Below is a caption that reads the tip of the day module allows the user to engage with a summary before reading an entire article.
Our gridded looping video component looks exactly the same as it did when using position:absolute, but can now handle more child elements without compromising the position of the play button.

Our button, even though placed over the video, is still within the document flow. Absolute positioning removes elements from the flow, creating an element without height, which subsequent elements may overlap. This isn’t necessarily an issue in this specific case, but keeping elements within the document flow can prevent layout problems, especially when needing to adapt to different viewport sizes.

Grid with pseudo-elements

The ::before and ::after pseudo-elements on a display:grid will also act like child grid items. The Boston Ruby Group website has a header that employs just that, using decorative gradient squares overlaid on a hero image.

A black and white photo of students ina classroom. Two gradient squares that are reddish to blue overlap the photo kitty corner to each other. White text overlaid on the image reads Boston Ruby Group, Boston Ruby Group helps you make connections, hack on projects, learn new things, and have fun with other Ruby enthusiasts.
Those decorative gradients are ::before and ::after elements within a grid.

<header class=”hero”>
  <picture class=”hero__media”>
    <source media="(min-width: 80em)" srcset="images.jpg">
  </picture>
  <div class=”hero__content”>
    // headline content
  </div>
</header>
.hero {
  grid-template-columns: repeat(7, 1fr);
  grid-template-rows: repeat(4, 0.5fr);
}

.hero::before,
.hero::after {
  content: "";
  display: block;
  opacity: 0.75;
}

.hero::before {
  background-image: linear-gradient(to bottom, var(--color-red), var(--color-teal));
  grid-column: 1 / 4;
  grid-row: 1 / 4;
}

.hero::after {
  background-image: linear-gradient(to bottom, var(--color-purple), var(--color-red));
  grid-column: 7 / 8;
  grid-row: 3 / 5;
  transform: translateY(2.5rem);
}

.hero__media {
  grid-column: 2 / 8;
  grid-row: 1 / 5;
}

.hero__content {
  grid-column: 3 / 6;
  grid-row: 4 / 5;
}

Each element in this example has an explicit start and end point along our 7-column, 4-row grid. The ::before gradient starts at the top left, ending on the 4th row and column. And to really hone in on that overlapped look, the hero image is offset underneath, starting on the second column line.

We can even break out of the grid a bit with the help of a transform. The :after gradient is pushed down 2.5rem below the last grid row line with the help of translateY.

A slightly transparent light blue rectangle with evenly spaced purple lines forming a grid over the rectangle. The grid is overlaid on a black and white photos with two gradient squares and text that reads Boston Ruby Group, Boston Ruby Group helps you make connections, hack on projects, learn new things, and have fun with other Ruby enthusiasts.
Our grid lines allow us to move each element to a precise location.

But can I still use position:absolute?

You do you, friend. The lovely thing about CSS and web development in general is that there isn’t usually one right way to achieve something. CSS, especially today, provides us with a lot of flexibility and creative license when it comes to layout decisions. You don’t necessarily have to adopt a new way of doing things if your tried and true pattern works well enough.