Positioning Overlay Content with CSS Grid

Not news to any web developer in 2021: CSS Grid is an incredibly powerful tool for creating complex, distinct two-dimensional modern web layouts.

Recently, I have been experimenting with CSS Grid and alignment properties to create component layouts that contain multiple overlapping elements. These layouts could be styled using absolute positioning and a mix of offset values (top, right, bottom, left), negative margins, and transforms. But, with CSS Grid, positioning overlay elements can be built using more logical, readable properties and values. The following are a few examples of where these grid properties come in handy.

It will help to read up on grid-template-areas and grid-area properties if you’re not yet familiar with them.

Expanding images inside limited dimensions

In the demo, there is a checkbox that toggles the overflow visibility so that we can see where the image dimensions expand beyond the container on larger viewport widths.

Here’s a common hero section with a headline overlapping an image. Although the image is capped with a max-width, it scales up to be quite tall on desktop. Because of this, the content strategy team has requested that some of the pertinent page content below the hero remain visible in the viewport as much as possible. Combining this layout technique and a fluid container max-height using the CSS clamp() function, we can develop something that adjusts based on the available viewport space while anchoring the hero image to the center of the container.

CSS clamp(), along with the min() and max() comparison functions, are well-supported in all modern browsers. Haven’t used them? Ahmad Shadeed conducts a fantastic deep dive in this article.

Open this Pen and resize the viewport width. Based on the image dimensions, the container height expands until it hits a maximum height. Notice that the image continues to grow while remaining centered in the container. Resize the viewport height and the container will flex between its max-height’s lower and upper bound values defined in the clamp() function.

Prior to using grid for the layout styles, I might have tried absolute positioning on the image and title, used an aspect ratio padding trick to create a responsive height, and object-fit to retain the ratio of the image. Something like this could get it there:

.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;
}

Maybe it’s possible to whittle the code down some more, but there’s still a good chunk of styling needed. Managing the same responsive layout with CSS Grid will simplify these layout style rules while making the code more readable. Check it out in the following iteration:

.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;
}

place-content: center instructs the image to continue growing out from the middle of the container. Remove this line and see that, while the image is still vertically centered via place-items, once the max-height is reached, the image will stick to the top of the container block and go on scaling beyond its bottom. Set place-content: end center and you’ll see the image spill over the top of the container.

This behavior may seem conceptually similar to applying object-fit: cover on an image as a styling method for preserving its intrinsic ratio while resizing to fill its content-box dimensions (it was utilized in the absolute position iteration). However, in this grid context, the image element governs the height of its parent and, once the parent’s max-height is reached, the image continues to expand, maintaining its ratio, and remains completely visible if the parent overflow is shown. object-fit could even be used with the aspect-ratio property here to create a consistent aspect ratio pattern for the hero image:

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

The overlay grid-area

Moving on to the container’s direct children, grid-area arranges each of them so that they overlap the same space. In this example, grid-template-areas with the named grid area makes the code a little more readable and works well as a pattern for other overlay-style layouts within a component library. That being said, it is possible to get this same result by removing the template rule and, instead of grid-area: container, using integers:

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

This is shorthand for grid-row-start, grid-column-start, grid-row-end, and grid-column-end. Since the siblings in this demo all share the same single row/column area, only the start lines need to be set for the desired result.

Setting place-self to place itself

Another common overlay pattern can be seen on image carousels. Interactive elements are often placed on top of the carousel viewport. I’ve extended the first demo and replaced the static hero image with a carousel.

Same story as before: This layout could fall back on absolute positioning and use integer values in a handful of properties to push and pull elements around their parent container. Instead, we’ll reuse the grid layout rulesets from the previous demo. Once applied, it appears as you might expect: all of the child elements are centered inside the container, overlapping one another.

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.

The next step is to set alignment values on individual elements. The place-self property—shorthand for align-self and justify-self—provides granular control over the position of a single item inside the container. Here are the layout styles altogether:

.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;
}

There’s just one small problem: The title and carousel dot indicators get pulled out into the overflow when the image exceeds the container dimensions.

To properly contain these elements within the parent, a grid-template-row value needs to be 100% of the container, set here as one fractional unit.

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

For this demo, I leaned into the the grid-template shorthand (which we will see again later in this article).

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

After providing that little update, the overlay elements stay within the parent container, even when the carousel images spread beyond the carousel’s borders.

Alignment and named grid-template-areas

Let’s use the previous overlay layout methods for one more example. In this demo, each box contains elements positioned in different areas on top of an image.

For the first iteration, a named template area is declared to overlay the children on the parent element space, similar to the previous demos:

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

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

The image and semi-transparent overlay now cover the box area, but these style rules also stretch the other items over the entire space. This seems like the right time for place-self to pepper these elements with some alignment magic!

.tag {
  place-self: start;
}

.title {
  place-self: center;
}

.tagline {
  place-self: end start;
}

.actions {
  place-self: end;
}

That‘s looking great! Every element is positioned in their defined places over the image as intended. Well, almost. There’s a bit of nuance to the bottom area where the tagline and action buttons reside. Hover over an image to reveal the tagline. This might look fine with a short string of text on a desktop screen, but if the tagline becomes longer (or the boxes in the viewport smaller), it will eventually extend behind the action 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.

To clean this up, the grid-template-areas use named areas for the tagline and actions. The grid-template-columns rule is introduced so that the actions container only scales to accommodate the size of its buttons while the tagline fills in the rest of the inline area using the 1fr value.

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

This can also be combined with the grid-template shorthand. The column values are defined after a slash, like so:

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

The grid-area is then converted to integers now that the “box” keyword has been removed.

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

Everything should look the way it did before. Now for the finishing touch. The tagline and actions keywords are set as their respective element grid-area values:

.tagline {
  grid-area: tagline;
  place-self: end start;
}

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

Now, when hovering over the cards in the demo, the tagline wraps to multiple lines when the text becomes too long, rather than pushing past the action buttons like it did before.

Named grid lines

Looking back at the first iteration of this code, I really liked having the default grid-area set to the box keyword. There’s a way to get that back.

I’m going add some named grid lines to the template. In the grid-template rule below, the first line defines the named template areas, which also represents the row. After the slash are the explicit column sizes (moved to a new line for readability). The [box-start] and [box-end] custom identifiers represent the box area.

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

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

Passing a name with the -start and -end syntax into brackets defines an area for that name. This name, known as a custom ident, can be anything but words from the CSS spec should be avoided.

Logical placement values

One of the really interesting parts to observe in this last example is the use of logical values, like start and end, for placing elements. If the direction or writing-mode were to change, then the elements would reposition accordingly.

When the “right to left” direction is selected from the dropdown, the inline start and end positions are reversed. This layout is ready to accommodate languages, such as Arabic or Hebrew, that read from right to left without having to override any of the existing CSS.

Wrapping up

I hope you enjoyed these demos and that they provide some new ideas for your own project layouts—I’ve compiled a collection of examples you can check out over at CodePen. The amount of power packed into the CSS Grid spec is incredible. Take a minute to reflect on the days of using floats and a clearfix for primitive grid row design, then return to the present day and behold the glorious layout and display properties of today‘s CSS. To make these things work well is no easy task, so let’s applaud the members of the CSS working group. The web space continues to evolve and they continue to make it a fun place to build.

Now let’s release container queries and really get this party started.


The post Positioning Overlay Content with CSS Grid appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

A Dynamically-Sized Sticky Sidebar with HTML and CSS

Creating page content that sticks to the viewport as you scroll, something like a jump-to-anchor menu or section headings, has never been easier. Throw a position: sticky into your CSS ruleset, set the directional offset (e.g. top: 0) and you’re ready to impress your teammates with minimal effort. Check out this CSS-Tricks article to see some real fancy sticky positioning use cases.

But sticky positioning can get a bit tricky, particularly when it comes to height and the dangerous situation of hiding content in a position that can’t be scrolled to. Let me set the stage and show you the problem and how I fixed it.

I recently worked on a desktop layout that we’re all familiar with: a main content area with a sidebar next to it. This particular sidebar contains action items and filters that are pertinent to the main content. As the page section is scrolled, this component remains fixed to the viewport and contextually accessible.

The layout styling was as easy to implement as I had mentioned earlier. But there was a catch: The height of the component would vary based on its content. I could have capped it with a max-height and set overflow-y: auto to make the component content scrollable. This worked well on my laptop screen and my typical viewport height, but in a smaller viewport with less vertical real estate, the sidebar’s height would exceed the viewport.

When the sticky sidebar height is larger than the viewport, some of its content becomes inaccessible until reaching the bottom of the container, when the element is no longer sticky.

That’s where things got tricky.

Thinking through solutions

I initially considered reaching for a media query. Perhaps I could use a media query to remove the sticky positioning and have the component sit relative to the top of the sidebar container. This would grant access to the entirety of its content. Otherwise, when scrolling the page, the sticky component’s content is cut off at the bottom of the viewport until I reach the end of its parent section.

Then I remembered that the height of the sticky component is dynamic.

What magic value could I use for my media query that would handle such a thing? Perhaps instead I could write a JavaScript function to check if the component flows beyond the viewport boundaries on page load? Then I could update the component’s height…

That was a possibility.

But what if the user resizes their window? Should I use that same function in a resize event handler? That doesn’t feel right. There must be a better way to build this.

Turns out there was and it involved some CSS trickery to get the job done!

Setting up the page section

I started with a flex display on the main element. A flex-basis value was set to the sidebar for a fixed desktop width. Then the article element filled the rest of the available horizontal viewport space.

If you’re curious about how I got the two containers to stack for smaller viewports without a media query, check out The Flexbox Holy Albatross trick.

I added align-self: start to the sidebar so its height wouldn’t stretch with the main article (stretch  is the default value). This gave my positioning properties the ability to cast their magic:

.sidebar {
  --offset: var(--space);
  /* ... */
  position: sticky;
  top: var(--offset);
}

Check that out! With these two CSS properties, the sidebar element sticks to the top of the viewport with an offset to give it some breathing room. Notice that the top value is set to a scoped CSS custom property. The --offset variable can now be reused on any child element inside the sidebar. This will come in handy later when setting the sticky sidebar component’s maximum height.

You can find a list of global CSS variable declarations in the CodePen demo, including the --space variable used for the offset value in the :root ruleset.

The sticky sidebar

Keep in mind that the component itself is not what is sticky; it’s the sidebar itself. General layout and positioning should typically be handled by the parent. This gives the component more flexibility and makes it more modular to use in other areas of the application.

Let’s dive into the anatomy of this component. In the demo, I’ve removed the decorative properties below to focus on the layout styles:

.component {
  display: grid;
  grid-template-rows: auto 1fr auto;
}


.component .content {
  max-height: 500px;
  overflow-y: auto;
}
  • This component uses CSS Grid and the pancake stack idea from 1-Line Layouts to configure the rows of this template. Both the header and footer (auto) adjust to the height of their children while the content (1fr, or one fraction unit) fills up the rest of the open vertical space.
  • A  max-height on the content limits the component’s growth on larger screen sizes. This is unnecessary if it’s preferred that the component stretch to fill the viewport height.
  • overflow-y: auto allows the content to be scrolled when necessary.

When the component is being used in the sidebar, a max-height is needed so that it doesn’t exceed the viewport height. The --offset previously scoped to the .sidebar class is doubled to create a margin on the bottom of the element that matches the top offset of the sticky sidebar:

.sidebar .component {
  max-height: calc(100vh - var(--offset) * 2);
}

That wraps up the assembly of this sticky sidebar component! After some decorative styles were applied, this prototype became ready for testing and review. Give it a try! Open up the demo in CodePen and click on the grid items to add them to the sidebar. Resize your browser window to see how the component flexes with the viewport while staying in view as you scroll the main content section.


This layout may work well on a desktop browser, but isn’t entirely ideal for smaller devices or viewport widths. However, the code here provides a solid foundation that makes it easy to add improvements to the UI.

One simple idea: A button could be affixed to the viewport window that, when clicked, jumps the page down to the sidebar content. Another idea: The sidebar could be hidden off-screen and a toggle button could slide it in from the left or right. Iteration and user testing will help drive this experience in the right direction.


The post A Dynamically-Sized Sticky Sidebar with HTML and CSS appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

React Slider with Parallax Hover Effects

Recently I experimented with building a content slider (or carousel, if that’s your fancy) using React. I wanted to create some unique position-based cursor effects when the user hovers over the active slide. This eventually led to the parallax effect you’ll see in the final demo.

This post will dive into the details of the slider’s components, the dynamic CSS variables used for the parallax hover effect, and some of the other properties that brought this project to life.

See the Pen React Slider w/ Hover Effect by Ryan Mulligan (@hexagoncircle) on CodePen.light

Component Setup

This React slider consists of three components: SliderSlide, and SliderControl. The SliderControl houses the button template used for the previous and next arrow controls. The Slider is the parent component that contains the methods for transitioning slides. Inside the Slider render template, an array of slide objects is iterated over and each slide’s data set is returned within a Slide child component using the map() method:

{slides.map(slide => {
  return (
    <Slide
      key={slide.index}
      slide={slide}
      current={current}
      handleSlideClick={this.handleSlideClick}
    />
  )
})}

Each of these rendered slides has the following properties:

  • A unique key (learn more about keys in React here). This key grabs index from the slide’s data.
  • A slide property equal to the slide object so the component can access that set of data.
  • The current property grabs the Slider’s current state value and controls the previous, current, and next classes being set on each slide.
  • handleSlideClick points to the Slider method of the same name to update the current value to the clicked slide’s index. This will animate the clicked slide into view.

Updating slide classes

The Slide element has additional classes set based on the current slide.

if (current === index) classNames += ' slide--current'
else if (current - 1 === index) classNames += ' slide--previous'
else if (current + 1 === index) classNames += ' slide--next'

In the code above, when current equals a slide’s index, that slide becomes active and is given a current class name. Adjacent sibling slides get previous and next class names. By adding these classes to their respective slides, unique hover styles can be applied.

Animation of previous and next slides with cursor changing as elements are hovered

On hover, the cursor changes based on the direction of the slide and that hovered element is pulled towards the current slide along the x-axis. As a result, the user receives some additional visual cues when they are interacting with those neighboring slides.

Slide Parallax Hover Effect

Now for the fun part! The Slide component contains methods that cast parallax magic. The onMouseMove event attribute is using the handleMouseMove method to update the x and y values as the user hovers over the slide. When the cursor is moved off of the slide, onMouseLeave calls handleMouseLeave to reset the x and y values and transition the slide elements back into place.

The x and y coordinates are calculated by finding the user’s cursor in the viewport and where it’s hovering in relation to the center of the slide element. Those coordinate values are assigned to CSS variables (--x and --y) that are then used in transforms to move the child elements around in the slide. In the following pen, click on the “display coordinates” checkbox and hover over the slide to see how the x and y values update to reflect your cursor’s position and movement.

See the Pen Cursor Movement Hover Effect by Ryan Mulligan (@hexagoncircle) on CodePen.light

The Parallax CSS

Let’s take a look at the CSS (Sass) being applied to some of these slide elements:

.slide:hover .slide__image-wrapper {
  --x: 0;
  --y: 0;
  --d: 50
		
  transform: 
    scale(1.025)
    translate(
      calc(var(--x) / var(--d) * 1px),
      calc(var(--y) / var(--d) * 1px)
  );
}

The slide__image-wrapper has overflow: hidden set so that the image can move beyond its wrapper container and hide some of itself beyond the wrapper boundaries. The wrapper container also has a faster transition-duration than the image. Now these elements animate at different speeds. I combined this with some fancy transform calculations and it developed some fluid, independent transitions.

Calculate those transforms

The translate(x, y) values are computed using the CSS calc() function. On the slide__image-wrapper, the --d property (the divisor) is set to 50, which yields a lower coordinate value and less of a push from the slide’s center. Now check out the slide__image transform:

.slide__image.slide--current {
  --d: 20;
	
  transform:
    translate(
      calc(var(--x) / var(--d) * 1px),
      calc(var(--y) / var(--d) * 1px)
    ); 
}

The divisor is changed to 20 on the slide__image so that the x and y values in the transform are higher and will push the image further away from the center of slide. Finally, the formula is multiplied by one pixel so that a unit gets applied to the value. Parallax achieved!

Try playing around with the --d values in the CSS and watch how the transitions change! Edit on Codepen.

Does it seem like the slide headline and button seem to move ever so slightly in the opposite direction of the image? Indeed they do! To achieve this, I multiplied the translate(x, y) calculations by negative pixel values instead:

.slide__content {
  --d: 60;
	
  transform: 
    translate(
      calc(var(--x) / var(--d) * -1px),
      calc(var(--y) / var(--d) * -1px)
    );
}

Moving the slides

Check out the Slider component render code in the final demo:

See the Pen React Slider w/ Hover Effect by Ryan Mulligan (@hexagoncircle) on CodePen.light

You’ll notice the slider__wrapper element surrounding the slides. This wrapper transitions back and forth along the x-axis as the user interacts with the slider. The values for this transform are set after the current slide’s index is multiplied by the amount of slides divided into 100.  I’ve added this in a variable on line 163 to keep the template a little cleaner:

'transform': `translateX(-${current * (100 / slides.length)}%)

In this example, there are 4 slides. Click the next arrow button or on the second slide (which has an index of 1) and it will pull the wrapper 25% to the left. Click on the third slide (index of 2), do the math (2 x 25), and watch it move the wrapper 50% to the left.

Some other tidbits

These are a few other features I’d like to quickly call out:

  • If a slide isn’t active, the pointer-events property is set to none. I chose to do this to avoid keyboard tab focusing on buttons inside inactive slides.
  • The parallax effect is only being applied to the current slide by declaring transforms when the slide--current class is present. Inactive slides have their own animations and shouldn’t have all that fun hover magic that the active slide has.
  • Images fade in when they are loaded using the imageLoaded method in the Slide component. This helps the initial load of a slide feel smoother instead of its image just popping in. A future iteration of this project will apply lazy loading as well (which is starting to roll out as a native browser feature; very exciting!)

How would you extend or refactor this idea? I’d love to read your thoughts and comments. Leave them below or reach out to me on Twitter.

React Slider with Parallax Hover Effects was written by Ryan Mulligan and published on Codrops.