Stacking elements with CSS Grid

Liam Crawshaw
Fender Engineering
Published in
8 min readMar 28, 2023

A guide to stacking DOM elements with z-index, Grid, and React

Photo by Sean Stratton on Unsplash

In modern web design, there are many scenarios where elements may need to overlap or stack. At Fender, we often need to stack components to create things like Modals, Banners, and Fixed Navbars. We can use the position and z-index properties for basic overlapping like this.

Thanks to CSS Grid, we can create more complex overlays. We gain much control over our multidimensional grids when we use position, z-index, and Grid together.

Simple Stacking

The z-index property takes an integer and specifies the elements’ order. An element with a higher z-index will appear on top of any elements with a lower z-index value.

To use the z-index property, we need to assign one of the following values to theposition property.

  1. absolute

Absolute positioning takes an element out of the document flow and positions it relative to its closest positioned-ancestor. If a positioned- ancestor does not exist, the position will be relative to the initial containing block. When used with the z-index property, this type of positioning is excellent for creating Modals.

CSS For Modal — using absolute positioning

.modal {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;

z-index: 999;
width: 100%;
height: 100vh;
}

(Modals can also use fixed positioning if you want the Modal message to scroll with the rest of the content)

2. fixed

Fixed positioning takes an element out of the document flow and positions it relative to the initial containing block established by the viewport. When used with the z-index property, this type of positioning is great for creating scrolling nav bars.

CSS For Scrolling Nav Bar — using fixed positioning

.scrolling-nav-bar {
position: fixed;
left: 0;
top: 0;

z-index: 999;
}
Scrolling Navbar on Fender Play

3. relative

Relative positioning keeps elements in the document flow, allowing us to use z-index to stack items with much more flexibility than fixed or absolute positioning. Relative positioning makes it possible to overlap sibling elements, even if there are significant changes between screen sizes.

We can use a negative margin to push our sibling elements together.

.top-element {
position: relative;
z-index: 1;
}

.bottom-element {
position: relative;
z-index: 0;
margin-top: -70px;
margin-left: 50px;
}
Items stacked with position and z-index

We found this stacking method helpful in putting text on top of SVG animations. SVG animations allow us to implement complex high-performant animations with less back and forth between design and engineering.

This is great, but if there is text in the animation, that text needs to be accessible to screen readers. To solve this, we excluded text from the SVG animation and added the text in a separate div. This allows us to position the SVG and text elements relatively, then use z-index to place the text on top of the animation.

Using just positioning and z-index will work, but should we have more items and need more control over how they’re stacked, using CSS Grid is a great option.

Accessibility Note

When stacking elements, be mindful of users who are interfacing with your site with screen readers or keyboards.

Just because an element appears to be hidden behind another element doesn’t mean it will be hidden from screen readers or keyboard users. If a z-index hidden item has interactive elements, users will still be able to interact with those elements.

If this is not what you intend, make sure to conditionally disable them or add visibility: hidden to a dynamic CSS class.

Grid Stacking

First, we must set our parent component to a grid display and declare how many rows and columns we want. Let’s create a 2x3 grid.

.grid-parent {
display: grid;
grid-template-rows: repeat(2, 1fr);
grid-template-columns: repeat(3, 1fr);
}

Then to stack items, we’ll need to place our grid items with the grid-area property. When we provide four values to this property, it will assign the following values:

  • grid-row-start
  • grid-column-start
  • grid-row-end
  • grid-column-end

These parameters take a column or row number separated by a backslash. To stack items, we can either overlap or repeat the values we provide to the grid-area property.

We can add the following grid-area values to stack two items in each of the 2x3 grid spaces.

.blue1 { grid-area: 1 / 1 / 2 / 2; }
.blue2 { grid-area: 1 / 2 / 2 / 3; }
.blue3 { grid-area: 1 / 3 / 2 / 4; }
.blue4 { grid-area: 2 / 1 / 3 / 2; }
.blue5 { grid-area: 2 / 2 / 3 / 3; }
.blue6 { grid-area: 2 / 3 / 3 / 4; }
.red1 { grid-area: 1 / 1 / 2 / 2; }
.red2 { grid-area: 1 / 2 / 2 / 3; }
.red3 { grid-area: 1 / 3 / 2 / 4; }
.red4 { grid-area: 2 / 1 / 3 / 2; }
.red5 { grid-area: 2 / 2 / 3 / 4; }
.red6 { grid-area: 2 / 3 / 3 / 4; }
Items stacked with Grid

With the CSS declared to create this Grid, our node tree will need to look something like this.

<div class="grid-parent">
<div class="blue1">item1</div>
<div class="blue2">item2</div>
<div class="blue3">item3</div>
<div class="blue4">item4</div>
<div class="blue5">item5</div>
<div class="blue6">item6</div>
<div class="red1">item1</div>
<div class="red2">item2</div>
<div class="red3">item3</div>
<div class="red4">item4</div>
<div class="red5">item5</div>
<div class="red6">item6</div>
</div>

But what if we need to render our grid items dynamically?

We can map each item from our response to an element and pass a counter variable to our map function. This will allow us to add dynamic class names to each element.

const bottomItems = blueSquares.map((item, i) => {
return (
<div className={`blue${i+1}`} key={i} >
{item.name}
</div>
);
});

const topItems = redSquares.map((item, i) => {
return (
<div className={`red${i+1}`} key={i} >
{item.name}
</div>
);
});

return (
<div className='grid-parent'>
{bottomItems}
{topItems}
<div/>
)

This will output the same node tree and class names needed to create the previous 2X3 Red and Blue Grid stack.

We can do some creative things with our dynamic class names to get all of the items stacked correctly, but we can take it further by reintroducing trusty old z-index.

Grid Stacking with z-index and React

Grid is great for initializing the order of our stacked items, but we can override this initial order by assigning relative positioning and z-index to our elements.

Let’s add the relative position to our div elements and create two classes named layer1 and layer2. We can assign them a z-indexof 0 and 1, respectively.

div {
position: relative;
}

.layer1 {
z-index: 0;
}

.layer2 {
z-index: 1;
}

Now we can dynamically assign these classes to determine the order of our elements.

We can accomplish this in React by creating a boolean state variable and using a ternary to assign one of the layer classes to our elements.

// state variable
const [switchOrder, setSwitchOrder] = useState(false);

// elements returned by component
<div className={switchOrder ? `layer2` : `layer1`}>
This item will be on top when switchOrder is true
</div>

<div className={switchOrder ? `layer1` : `layer2`}>
This item will be on bottom when switchOrder is true
</div>

When we update the value of switchOrder, we will dynamically set which elements are on top.

Let’s add a button and clickHandler to our 2X3 Grid to accomplish this.

import './App.css';
import { useState } from 'react';

export default function App() {
const [switchOrder, setSwitchOrder] = useState(false);

const bottomItems = blueSquares.map((item, i) => {
return (
<div className={switchOrder ? `layer1` : `layer2`} key={i}>
{item.name}
</div>
);
});

const topItems = redSquares.map((item, i) => {
return (
<div className={switchOrder ? `layer2` : `layer1`} key={i}>
{item.name}
</div>
);
});

const handleChangeItemOrder = () => {
setSwitchOrder(!switchOrder);
};

return (
<>
<section className="grid-parent">
{bottomItems}
{topItems}
</section>
<div className="button-container">
<button onClick={() => handleChangeItemOrder()}>Switch Items</button>
</div>
</>
)
}
Toggling order of grid items with state and z-index

This exercise should get you stacking items with Grid, CSS, and React. Feel free to copy the complete code below and experiment by adding different layouts and transitions! Here are the final CSS and JSX files for our Grid:

App.jsx:

import './App.css';
import { useState } from 'react';

export default function App() {

const [switchOrder, setSwitchOrder] = useState(false);

const blueSquares = [
{ name: 'blue item 1' },
{ name: 'blue item 2' },
{ name: 'blue item 3' },
{ name: 'blue item 4' },
{ name: 'blue item 5' },
{ name: 'blue item 6' },
];

const redSquares = [
{ name: 'red item 1' },
{ name: 'red item 2' },
{ name: 'red item 3' },
{ name: 'red item 4' },
{ name: 'red item 5' },
{ name: 'red item 6' },
];

const bottomItems = blueSquares.map((item, i) => {
return (
<div className={switchOrder ? `red${i + 1} blue-box-group layer1` : `red${i + 1} blue-box-group layer2`} key={i}>
{item.name}
</div>
);
});

const topItems = redSquares.map((item, i) => {
return (
<div className={switchOrder ? `blue${i + 1} red-box-group layer2` : `blue${i + 1} red-box-group layer1`} key={i}>
{item.name}
</div>
);
});

const handleChangeItemOrder = () => {
setSwitchOrder(!switchOrder);
};

return (
<>
<section className="grid-parent">
{bottomItems}
{topItems}
</section>
<div className="button-container">
<button onClick={() => handleChangeItemOrder()}>Switch Items</button>
</div>
</>
)
}

App.css

html, body {
height: 100%;
width: 100%;
}

.grid-parent {
display: grid;
grid-template-rows: repeat(2, 1fr);
grid-template-columns: repeat(3, 1fr);
}

.blue-box-group {
padding: 18px;
height: 82px;
width: 82px;
border: solid black 1px;
background-color: lightblue;
box-shadow: rgba(0, 0, 0, 0.15) 1.95px 1.95px 2.6px;
position: relative;
}

.red-box-group {
padding: 18px;
height: 82px;
width: 82px;
border: solid black 1px;
margin-top: 20px;
margin-bottom: 4px;
background-color: red;
margin-left: 10px;
box-shadow: rgba(0, 0, 0, 0.25) 0px 14px 28px, rgba(0, 0, 0, 0.22) 0px 10px 10px;
position: relative;
}

.layer1 {
z-index: 0;
}

.layer2 {
z-index: 1;
}

.button-container {
width: 100px;
margin: 0 auto;
margin-top: 24px;
}

.red1 { grid-area: 1 / 1 / 2 / 2; }
.red2 { grid-area: 1 / 2 / 2 / 3; }
.red3 { grid-area: 1 / 3 / 2 / 4; }
.red4 { grid-area: 2 / 1 / 3 / 2; }
.red5 { grid-area: 2 / 2 / 3 / 3; }
.red6 { grid-area: 2 / 3 / 3 / 4; }
.blue1 { grid-area: 1 / 1 / 2 / 2; }
.blue2 { grid-area: 1 / 2 / 2 / 3; }
.blue3 { grid-area: 1 / 3 / 2 / 4; }
.blue4 { grid-area: 2 / 1 / 3 / 2; }
.blue5 { grid-area: 2 / 2 / 3 / 4; }
.blue6 { grid-area: 2 / 3 / 3 / 4; }

--

--