Z-stack

Z 스택

Overlap several elements in the same grid-area to stack layers without ever reaching for position: absolute.

Also known as: Grid stacking contextOverlap via grid-area
···
html
<div class="wrap">
  <div class="zstack" id="zstack">
    <div class="layer l1">Base</div>
    <div class="layer l2">Badge</div>
    <div class="layer l3"></div>
  </div>
  <div class="tag" id="tag">front: Base</div>
</div>
css
.wrap{display:flex;flex-direction:column;align-items:center;gap:10px}
.zstack{display:grid;width:min(42vmin,190px);aspect-ratio:1;place-items:center}
.layer{grid-area:1/1;border-radius:50%;display:grid;place-items:center;font-weight:800;
  transition:transform .5s cubic-bezier(.34,1.56,.64,1),opacity .5s}
.l1{width:100%;height:100%;background:var(--surface);border:2px solid var(--line);color:var(--muted);font-size:11px}
.l2{width:52%;height:52%;background:var(--accent);color:#fff;font-size:11px}
.l3{width:80%;height:80%;border:3px dashed var(--accent-2);background:transparent}
.layer.front{transform:scale(1.12)}
.layer.back{opacity:.5}
js
const l1 = document.querySelector('.l1');
const l2 = document.querySelector('.l2');
const l3 = document.querySelector('.l3');
const tag = document.getElementById('tag');
const order = [
  [l1, 'Base'],
  [l3, 'Ring'],
  [l2, 'Badge'],
];
let i = 0;
function apply() {
  [l1, l2, l3].forEach((l) => l.classList.add('back'));
  const [front, name] = order[i % order.length];
  front.classList.remove('back');
  front.classList.add('front');
  setTimeout(() => front.classList.remove('front'), 480);
  tag.textContent = 'front: ' + name;
  i++;
}
apply();
setInterval(apply, 1500);

The traditional way to overlap elements is position: relative on the parent with position: absolute; inset: 0 on each child. Because that pulls children completely out of normal flow, sizing them with percentages requires the parent to have an explicit height, and the technique doesn't mesh well with flex or grid alignment systems.

CSS Grid offers a simpler route: just display: grid on the parent, and give every child you want overlapped the same grid-area (or the same grid-row / grid-column). With no named areas declared, an implicit first cell (1 / 1) is created, and any children assigned to that cell land on top of each other in the same spot. Because nothing touches position, each layer can still use grid alignment properties like place-self or justify-self normally.

Stacking order (which layer is on top) follows DOM order, and can be overridden with z-index when needed. For a card where a badge sits on an image, or a widget where a progress ring circles an icon — cases where a few layers need to align exactly on the same spot — this pattern is less code than position: absolute and holds up better across responsive changes.

When to use

Use it when a handful of layers need to align at exactly the same spot — a badge, an icon, an overlay. When layers need to scatter to genuinely different positions, position: absolute is still the right tool.