Four Ways to Position Overlays with CSS Grid

CSS Grid is more than a tool for building page-level layouts. Its alignment properties and named areas work just as well for laying out components where elements need to overlap. Where a traditional approach might reach for absolute positioning with a stack of offset values, negative margins, and transforms, Grid offers something more readable: elements simply share the same grid area, and alignment properties do the rest.

The examples that follow assume basic familiarity with grid-template-areas and grid-area. All of the patterns rely on a parent container with child elements layered using the same row and column tracks.

Keep an Image Centered and Overflowing

A hero section often faces a tradeoff: a large image that scales with the viewport can push content below the fold. One fix is to cap the container’s height and let the image continue growing, anchored to the center so the overflow stays equally distributed on both sides.

A non-Grid solution might use absolute positioning for the image and headline, a padding-based aspect-ratio trick, and object-fit to preserve the image’s proportions:

.container {
  position: relative;
  max-height: clamp(400px, 50vh, 600px);
}

.container::before {
  content: '';
  display: block;
  padding-top: 52.25%;
}

.container > * {
  max-width: 1000px;
}

.container .image {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.container .title {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 100%;
  text-align: center;
}

That works, but it takes a pile of properties to communicate a fairly simple idea. Consider the alternative with Grid:

.container {
  display: grid;
  grid-template: "container";
  place-items: center;
  place-content: center;
  overflow: hidden;
  max-height: clamp(450px, 50vh, 600px);
}

.container > * {
  grid-area: container;
  max-width: 1000px;
}

With the container set as a grid, place-content: center does the heavy lifting. The image begins at the container’s center point and expands outward in both directions when the parent’s max-height is reached. Without it, the image would stay centered via place-items but clip out from the bottom once the height limit hits. Switch to place-content: end center and the image spills past the top instead.

This behavior differs from object-fit: cover, which scales an image to fill its box. Here, the image drives the parent’s height and, when that height is limited, keeps its full proportions while overflowing in both directions. You can still pair it with the aspect-ratio property if you want a set ratio for the image:

.container .image {
  width: 100%;
  height: auto;
  object-fit: cover;
  aspect-ratio: 16 / 9;
}

Shared Area with grid-area

The container’s children overlap by assigning them all the same named grid area. Giving each child the same name repeats a bit of code, but it reads clearly once you get past that repetition:

.container > * {
  grid-area: 1 / 1;
}

On a single-row, single-column layout, you can let the names go and use lines instead. All children share the same track, so only the start lines matter. The integer form above is shorthand for setting grid-row-start, grid-column-start, and skipping the end values entirely.

Individual Placement with place-self

Overlay patterns show up frequently with carousel controls, where buttons and indicators sit on top of the image track. The shared-area trick starts them all in the center:

Picture of a cute light brown puppy with the words Welcome to the Snuggle Zone on top in white. The text overlaps over text elements and is hard to read.
With place-items: center declared on the container, all of its direct children will overlap one another.

Adjusting individual elements requires per-item alignment. The place-self property — shorthand for align-self and justify-self — moves a single child around the container:

.container {
  display: grid;
  grid-template:"container";
  place-items: center;
  place-content: center;
  overflow: hidden;
  max-height: clamp(450px, 50vh, 600px);
}

.container > * {
  grid-area: container;
  max-width: 1000px;
}

.title {
  place-self: start center;
}

.carousel-control.prev {
  place-self: center left;
}

.carousel-control.next {
  place-self: center right;
}

.carousel-dots {
  place-self: end center;
}

The one issue: elements like a title or dot indicators get drawn outside the viewport when the images grow past the container’s bounds. Setting a single row track to fill the entire height keeps those overlays inside, even as the image underneath keeps expanding:

.container {
  grid-template-areas: "container";
  grid-template-rows: 1fr;
}

That approach also works if you prefer the more compact grid-template shorthand, which combines the track definitions in one line:

.container {
  grid-template: "container" 1fr;
}

Named Areas and the Two-Column Bottom Row

A card layout with a tagline and action buttons over an image is another case where the same rules apply. Give the image an explicit area so it fills the box entirely, then let the overlay elements line up on top:

.box {
  display: grid;
  grid-template-areas: "box";
}

.box > *,
.box::before {
  grid-area: box;
}

Once everything stretches to fill, place-self positions the layered children into their intended corners. The tricky member is the bottom strip where a tagline meets buttons:

A two by two grid of images with text overlaid on top, as well as a tag label in the top right corner and, tagline in the bottom left corner and actions to like and share in the bottom right corner of each one.
Note how the tagline in the first box on the second row overlaps the action buttons.

Two columns decode it. The full-height image area spans the row, while 1fr lets the tagline expand, and the action buttons take only as much space as they need:

.box {
  display: grid;
  grid-template-areas: "tagline actions";
  grid-template-columns: 1fr auto;
}

The same rule fits inside the grid-template shorthand with column values after the slash:

.box {
  grid-template: "tagline actions" / 1fr auto;
}

Switch the children over to integer grid-area values, and then only the tagline and action column need named areas:

.box > *,
.box::before {
  grid-area: 1 / 1 / -1 / -1;
}
.tagline {
  grid-area: tagline;
  place-self: end start;
}

.actions {
  grid-area: actions;
  place-self: end;
}

With named areas in place, longer tagline text wraps onto multiple lines instead of sliding behind the buttons.

Bringing Back the Box Keyword

The first card layout had the default grid-area: box for every child. That keyword disappears if you remove the named area. There is a way to keep it. Named grid lines can represent the box’s boundaries:

.box {
  display: grid;
  grid-template: 
    [box-start] "tagline actions" [box-end] /
    [box-start] 1fr auto [box-end];
}

.box > *,
.box::before {
  grid-area: box;
}

Writing [box-start] and [box-end] in the template defines an implicit area named box — with some catches. The custom identifier works for whatever you want to call it, provided the name isn’t a reserved word from the CSS spec.

Logical Values Hold Up in RTL Layouts

The final example uses start and end for alignment rather than physical left and right values. Those keywords follow the inline direction, so flipping the parent’s direction to rtl mirrors the whole layout without a single override. Overlays in languages read from right to left position automatically — no custom CSS written just for that case.

The demos in this article cover the common cases: image heroes with overflow, carousel controls on top of a viewport, and rich cards combining multiple overlays. The same shared-area pattern appears in all of them, and one or two alignment properties handle what used to require offset arithmetic and negative margins.