Managing z-indexes within CSS-in-JS
ReactJS CSS JavaScriptManaging z-indexes can be tricky. You'll undoubtedly be familiar with examples like these:
.header {
position: sticky;
z-index: 100; /* Keep sticky header above other content */
}
.popup {
z-index: 99999; /* Must be the absolute top */
}
While it works, it's not particularly elegant. Searching through your codebase for all z-index
values could easily yield something like [-1, 1, 2, 3, 4, 5, 10, 11, 19, 100, 200, 299, ..., 99999]
. Have fun choosing the next number in that sequence (or worse, adding something between 2
and 3
).
If your CSS is generated using a CSS-in-JS library (e.g. styled-components, Linaria, JSS...) a fairly clean way to organize your z-indexes is by defining them in an array:
const zIndexOrder = [
'body',
'header',
'popup',
];
const zIndexes = zIndexOrder.reduce(
(acc, current, index) => {
acc[current] = index;
return acc;
},
{}
);
export default zIndexes;
Then use it in your component as follows:
import styled from 'styled-components';
import zIndexes from '../style/zIndexes';
const MyHeader = styled.header`
z-index: ${zIndexes.header};
`;
The neat thing about this is adding an additional z-index becomes trivial:
const zIndexOrder = [
'body',
'aside', // New
'header',
'popup',
];
The z-index number values automatically update.
TypeScript variant #
In TypeScript you could do the following:
const zIndexOrder = [
'body',
'header',
'popup',
] as const;
type ZIndexValues = typeof zIndexOrder[number];
type ZIndexRecord = Record<ZIndexValues, number>;
const zIndexes = zIndexOrder.reduce(
(acc: ZIndexRecord, current: ZIndexValues, index: number) => {
acc[current] = index;
return acc;
},
{} as ZIndexRecord
);
export default zIndexes;
It's a bit convoluted but does guarantee type-safety in your component:
import styled from 'styled-components';
import zIndexes from '../style/zIndexes';
// The ollowing gives an error:
// TS2551: Property 'headr' does not exist on type 'ZIndexRecord'. Did you mean 'header'?
const MyHeader = styled.header`
z-index: ${zIndexes.headr};
`;
Beware your stacking contexts #
A z-index is relative to other elements within what's called a "stacking context". The main root element (<html />
) creates such a stacking context. However, there are various ways you can (usually unknowingly) create a new stacking context. MDN lists all possible causes.
As a consequence, an element with a high z-index can be rendered beneath an element with a low z-index. This can be counter-intuitive and confusing at first.
The most common cause for this problem I've seen is when using position
on some parent element while trying to set a z-index
on a child element.
For example:
<div class="red-parent">
<div class="red-child">
Red
</div>
</div>
<div class="blue-parent">
<div class="blue-child">
Blue
</div>
</div>
.red-parent {
position: absolute; /* this causes a new stacking context */
border: 2px dashed DarkRed;
}
.red-child {
padding: 2rem;
background-color: IndianRed;
z-index: 999; /* despite this high value, red still appears below blue */
}
.blue-parent {
position: absolute; /* this causes a new stacking context */
top: 4rem;
left: 3rem;
border: 2px dashed DarkBlue;
}
.blue-child {
padding: 2rem;
background-color: CornflowerBlue;
}
Despite .red-child { z-index: 999 }
the blue block appears on top. This is caused by .red-parent { position: absolute }
which creates a new stacking context for all of .red-parent
's children.
A new stacking context only affects the element's children; the z-index
of the positioned element itself is still part of the original stacking context.
That is why blue appears on top of red; both .blue-parent
and .red-parent
have the default z-index: 0
. Because they have the same z-index value within the same root stacking context the last element in HTML is drawn on top (see Stacking without the z-index property).
Usually I fix this issue by moving the z-index to the positioned element. I.e:
.red-parent {
position: absolute;
border: 2px dashed DarkRed;
z-index: 999; /* move z-index to the positioned element */
}
.red-child {
padding: 2rem;
background-color: IndianRed;
/* remove z-index from child */
}
Why does this matter for the zIndexes
-array approach? #
Having only one zIndexes
-array would logically correspond to one stacking context: the root stacking context.
But; since it's often unclear when a new stacking context is created you could easily end up having one zIndexes
-array that (tries) to manage multiple stacking contexts. This could lead to unexpected results.
In most layouts I find z-indexes can be limited to the main positioned element and one zIndexes
-array is all I need for the whole app.
If I really do need a separate stacking context, I'll define a second zIndexes
-array for that secondary stacking context (e.g. zMenuIndexes
in Menu.js
).
My reasoning for this is it is pretty rare to require z-indexes in a non-root stacking context; so it is fine -and arguably preferable- to make that explicit rather than trying to manage all zIndexes
in a single array.
SASS #
You can do something similar with SASS as well: Handling z-index with SASS.