grid – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Fri, 11 Nov 2022 14:43:14 +0000 en-US hourly 1 https://wordpress.org/?v=6.1.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 grid – CSS-Tricks https://css-tricks.com 32 32 45537868 CSS Grid and Custom Shapes, Part 3 https://css-tricks.com/css-grid-and-custom-shapes-part-3/ https://css-tricks.com/css-grid-and-custom-shapes-part-3/#comments Fri, 11 Nov 2022 14:42:04 +0000 https://css-tricks.com/?p=374826 After Part 1 and Part 2, I am back with a third article to explore more fancy shapes. Like the previous articles, we are going to combine CSS Grid with clipping and masking to create fancy layouts for image …


CSS Grid and Custom Shapes, Part 3 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
After Part 1 and Part 2, I am back with a third article to explore more fancy shapes. Like the previous articles, we are going to combine CSS Grid with clipping and masking to create fancy layouts for image galleries.

CSS Grid and Custom Shapes series

Should I read the previous articles before?

It’s not mandatory but highly recommended to cover as many tricks as possible. You can also read them in any order, but following along in chronological is a good idea to see how we arrived here.

Enough talking, let’s jump straight to our first example.

Before digging into the CSS, let’s check the markup:

<div class="gallery">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
</div>

Nothing but a few <img> tags in a div wrapper, right? Remember, the main challenge for this series is to work with the smallest amount of HTML possible. All the examples we’ve seen throughout this series use the exact same HTML markup. No extra divs, wrappers, and whatnot. All that we need are images contained in a wrapper element.

Let’s check the CSS now:

.gallery {
  --g: 6px; /* the gap */

  display: grid;
  width: 450px; /* the size */
  aspect-ratio: 1; /* equal height */
  grid: auto-flow 1fr / repeat(3, 1fr);
  gap: var(--g);
}
.gallery img:nth-child(2) {
  grid-area: 1 / 2 / span 2 / span 2;
}
.gallery img:nth-child(3) {
  grid-area: 2 / 1 / span 2 / span 2;
}

Basically, this is a square grid with three equal columns. From there, all that’s happening is the second and third images are explicitly placed on the grid, allowing the first and last images to lay out automatically around them.

This automatic behavior is a powerful feature of CSS Grid called “auto-placement”. Same thing with the number of rows — none of them are explicitly defined. The browser “implicitly” creates them based on the placement of the items. I have a very detailed article that explores both concepts.

You might be wondering what’s going on with those grid and grid-area property values. They look strange and are tough to grok! That’s because I chose the CSS grid shorthand property, which is super useful but accepts an unseemly number of values from its constituent properties. You can see all of them in the Almanac.

But what you really need to know is this:

grid: auto-flow 1fr / repeat(3, 1fr);

…is equivalent to this:

grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 1fr;
DevTools style rules for the grid property.
You can also use your favorite DevTools for further proof.

Same for the grid-area property. If we open DevTools and inspect our declaration: grid-area: 1/2/span 2/span 2; you will get the following:

grid-area: 1 / 2 / span 2 / span 2;

…that is the same as writing all this out:

grid-row-start: 1; /* 1st row */
grid-column-start: 2; /* 2nd column */
grid-row-end: span 2; /* take 2 rows */
grid-column-end: span 2; /* take 2 columns */

Same deal for the other grid-area declaration. When we put it all together, here’s what we get:

The different images labeled by number on the grid.

Yes, the second and third images are overlapped in the middle. That’s no mistake! I purposely spanned them on top of one another so that I can apply a clip-path to cut a portion from each one and get the final result:

Showing the effect with and without clip-path.

How do we do that? We can cut the bottom-left corner of the second image (img:nth-child(2)) with the CSS clip-path property:

clip-path: polygon(0 0, 100% 0, 100% 100%, calc(50% + var(--g) / 4) 100%, 0 calc(50% - var(--g) / 4))

And the top-right corner of the third one:

clip-path: polygon(0 0, calc(50% - var(--g) / 4) 0, 100% calc(50% + var(--g) / 4), 100% 100%, 0 100%);

I know, I know. That’s a lot of numbers and whatnot. I do have an article that details the technique.

That’s it, we have our first grid of images! I added a grayscale filter on the <img> selector to get that neat little hover effect.

The Split Image Reveal

Let’s try something different. We can take what we learned about clipping the corner of an image and combine it with a nice effect to reveal the full image on hover.

The grid configuration for this one is less intense than the last one, as all we need are two overlapping images:

.gallery {
  display: grid;
}
.gallery > img {
  grid-area: 1 / 1;
  width: 350px; /* the size */
  aspect-ratio: 1; /* equal height */
}

Two images that are the same size are stacked on top of each other (thanks to grid-area: 1 / 1).

The hover effect relies on animating clip-path. We will dissect the code of the first image to see how it works, then plug the same thing into the second image with updated values. Notice, though that we have three different states:

  1. When no images are hovered, half of each image is revealed.
  2. When we hover over the first image, it is more fully revealed but retains a small corner clip.
  3. When we hover over the second image, the first one has only a small triangle visible.
Showing the three clipping states of the hover effect.

In each case, we have a triangular shape. That means we need a three-point polygon for the clip-path value.

What? The second state isn’t a triangle, but more of a square with a cut corner.

You are right, but if we look closely we can see a “hidden” triangle. Let’s add a box-shadow to the images.

A ha! Did you notice it?

Showing the transition between states with the overflow shape revealed to explain how it works.

What sort of magic is this? It’s a little known fact that clip-path accepts values outside the 0%-100% range, which allows us to create “overflowing” shapes. (Yes, I just coined this. You’re welcome.) This way, we only have to work with three points instead of the five it would take to make the same shape from the visible parts. Optimized CSS for the win!

This is the code after we plug in the polygon values into the clip-path property:

.gallery > img:first-child {
  clip-path: polygon(0 0, calc(100% + var(--_p)) 0 , 0 calc(100% + var(--_p)))
}
.gallery > img:last-child {
  clip-path: polygon(100% 100%, 100% calc(0% - var(--_p)), calc(0% - var(--_p)) 100%)
}

Notice the --_p variable. I’m using that to optimize the code a bit as we add the hover transition. Instead of updating the whole clip-path we only update this variable to get the movement. Here is a video to see how the points should move between each state:

We can take slap a transition on the <img> selector, then update the --_p variable on the states to get the final effect:

.gallery {
  --g: 8px; /* the gap */
}
.gallery > img {
  /* etc. */
  --_p: calc(-1 * var(--g));
  transition: .4s .1s;
}
.gallery:hover > img:last-child,
.gallery:hover > img:first-child:hover{
  --_p: calc(50% - var(--g));
}
.gallery:hover > img:first-child,
.gallery:hover > img:first-child:hover + img {
  --_p: calc(-50% - var(--g));
}

If we don’t consider the gap (defined as --g in the code) between the images, then the three values of --_p are 0%, 50%, and -50%. Each one defines one of the states we explained previously.

The Pie Image Reveal

Let’s increase the difficulty level from that last one and try to do the same trick but with four images instead of two.

Cool, right? Each image is a quarter of a circle and, on hover, we have an animation that transforms an image into a full circle that covers the remaining images. The effect may look impossible because there is no way to rotate points and transform them to fill the circle. In reality, though, we are not rotating any points at all. It’s an illusion!

For this example, I will only focus on the clip-path animation since the configuration of the grid is the same as the previous example: four equally-sized images stacked on top of each other.

And a video worth a boring and long explanation:

The clip-path is formed by seven points, where three of them are in a fixed position and the others move as shown in the video. The effect looks less cool when it’s running slowly but we can see how the clip-path morphs between shapes.

The effect is a little better if we add border-radius and we make it faster:

And by making it even faster like in the original example, we get the perfect illusion of one quarter of a circle morphing into a full circle. Here’s the polygon value for our clip-path on the first image in the sequence:

.gallery > img:nth-child(1) {
  clip-path: polygon(50% 50%, calc(50% * var(--_i, 0)) calc(120% * var(--_i, 0)), 0 calc(100% * var(--_i, 0)),0 0, 100% 0, 100% calc(100% * var(--_i, 0)), calc(100% - 50% * var(--_i, 0)) calc(120% * var(--_i, 0)));
}
.gallery > img:hover {
 --_i: 1;
}

As usual, I am using a variable to optimize the code. The variable will switch between 0 and 1 to update the polygon.

The same goes for the others image but with a different clip-path configuration. I know that the values may look hard to decipher but you can always use online tools like Clippy to visualize the values.

The Mosaic of Images

You know mosaics, right? It’s an art style that creates decorative designs out of smaller individual pieces, like colored stones. But it can also be a composite image made up of other smaller images.

And, you guessed it: we can totally do that sort of thing in CSS!

First, let’s imagine what things are like if clip-path were taken out of the mix and all we had were five overlapping images:

I am cheating a little in this video because I am inspecting the code to identify the area of each image, but this is what you need to do in your head. For each image, try to complete the missing part to see the full rectangle and, with this, we can identify the position and size of each one.

We need to find how many columns and rows we need for the grid:

  1. We have two big images placed next to each other that each fill half the grid width and the full grid height. That means will probably need two columns (one for both images) and one row (for the full height of the grid).
  2. We have the image in the middle that overlaps the two other images. That means we actually need four columns instead of two, though we still only need the one row.
  3. The last two images each fill half the grid, just like the first two images. But they’re only half the height of the grid. We can use the existing columns we already have, but we’re going to need two rows instead of one to account for these images being half the grid height.
That leaves us with a tidy 4×2 grid.

I don’t want you to think that the way I sliced this up is the only way to do it. This is merely how I’ve made sense of it. I am sure there are other configurations possible to get the same layout!

Let’s take that information and define our grid, then place the images on it:

.gallery {
  display: grid;
  grid: repeat(2, 1fr) / repeat(4, 1fr); 
  aspect-ratio: 2;
}
.gallery img:nth-child(1) {
  grid-area: 1 / 1 / span 2 / span 2;
}
.gallery img:nth-child(2) {
  grid-area: 1 / 2 / span 2 / span 2;
}
.gallery img:nth-child(3) {
  grid-area: span 2 / span 2 / -1 / -1;
}
.gallery img:nth-child(4) {
  grid-area: 2 / 1 / span 1 / span 2;
}
.gallery img:nth-child(5) {
  grid-area: span 1 / span 2 / -1 / -1;
}

I think you get the idea of what’s happening here now that we’ve seen a few examples using the same approach. We define a grid and place images on it explicitly, using grid-area so the images overlap.

OK, but the aspect-ratio is different this time.

It is! If you get back to the reasoning we made, we have the first two images that are square next to each other having the same size. This means that the width of the grid needs to be equal to twice its height. Hence, aspect-ratio: 2.

Now it’s time for the clip-path values. We have four triangles and a rhombus.

Showing the three unique shapes and the clip-path values that create them.
We’re only showing the three unique shapes we’re making instead of the five total shapes.

Again, I’m using Clippy for all this math-y stuff. But, honestly, I can write many simple shapes by hand, having spent several years working closely with clip-path, and I know you can too with practice!

The Complex Mosaic of Images

Let’s increase the difficulty and try another mosaic, this time with less symmetry and more complex shapes.

Don’t worry, you will see that it’s the same concept as the one we just made! Again, let’s imagine each image is a rectangle, then go about defining the grid based on what we see.

We’ll start with two images:

They are both squares. The first image is equal to half the size of the second image. The first image takes up less than one half of the grid width, while the second image takes up more than half giving us a total of two columns with a different size (the first one is equal to half the second one). The first image is half the height, so let’s automatically assume we need two rows as well.

Let’s add another image to the layout

This one makes things a bit more complex! We need to draw some lines to identify how to update the grid configuration.

We will move from a 2×2 grid to four columns and three rows. Pretty asymmetric, right? Before we try to figure out that complete sizing, let’s see if the same layout holds up when we add the other images.

Looks like we still need more rows and columns for everything to fall into place. Based on the lines in that image, we’re going to have a total of five columns and four rows.

The logic is simple even though the layout is complex, right? We add the images one by one to find the right configuration that fits everything. Now we need to identify the size of each column and row.

If we say the smallest row/column is equal to one fraction of the grid (1fr) we will get:

grid-template-columns: 1fr 1fr 2fr 3fr 5fr;

…for the columns, and:

grid-template-rows: 3fr 1fr 2fr 2fr;

…for the rows. We can consolidate this using the grid shorthand property again:

grid: 3fr 1fr 2fr 2fr / 1fr 1fr 2fr 3fr 5fr;

You know the drill! Place the images on the grid and apply a clip-path on them:

.gallery img:nth-child(1) {
  grid-area: 1 / 1 /span 2 / span 3;
  clip-path: polygon(0 0, 100% 0, 0 100%);
}
.gallery img:nth-child(2) {
  grid-area: 1/2/span 3/span 3;
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
.gallery img:nth-child(3) {
  grid-area: 1 / span 2 / -1 / -1;
  clip-path: polygon(0 0, 100% 0, 100% 100%);
}
.gallery img:nth-child(4) {
  grid-area: span 3 / 1 / -1 / span 3;
  clip-path: polygon(25% 0, 100% 60%, 50% 100%, 0 100%, 0 20%);
}
.gallery img:nth-child(5) {
  grid-area: span 3/span 3/-1/-1;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

We can stop here and our code is fine, but we will do a little more to optimize the clip-path values. Since we don’t have any gaps between our images, we can use the fact that our images overlap to slim things down. Here is a video to illustrate the idea:

As you can see, the image in the middle (the one with the camera) doesn’t need a clip-path. because the other images overlap it, giving us the shape without any additional work! And notice that we can use the same overflowing three-point clip-path concept we used earlier on the image in the bottom-left to keep the code smaller there as well.

In the end, we have a complex-looking grid of images with only four clip-path declarations — all of them are three-point polygons!

Wrapping up

Wow, right? I don’t know about you, but I never get bored of seeing what CSS can do these days. It wasn’t long ago that all of this would have taken verbose hackery and definitely some JavaScript.

Throughout this series, we explored many, many different types of image grids, from the basic stuff to the complex mosaics we made today. And we got a lot of hands-on experience working with CSS clipping — something that you will definitely be able to use on other projects!

But before we end this, I have some homework for you…

Here are two mosaics that I want you to make using what we covered here. One is on the “easier” side, and the other is a bit tricky. It would be really awesome to see your work in the comments, so link them up! I’m curious to see if your approach is different from how I’d go about it!


CSS Grid and Custom Shapes, Part 3 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-grid-and-custom-shapes-part-3/feed/ 2 374826
How I Made a Pure CSS Puzzle Game https://css-tricks.com/how-i-made-a-pure-css-puzzle-game/ https://css-tricks.com/how-i-made-a-pure-css-puzzle-game/#comments Fri, 09 Sep 2022 13:21:29 +0000 https://css-tricks.com/?p=372952 I recently discovered the joy of creating CSS-only games. It’s always fascinating how HTML and CSS are capable of handling the logic of an entire online game, so I had to try it! Such games usually rely on the ol’ …


How I Made a Pure CSS Puzzle Game originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I recently discovered the joy of creating CSS-only games. It’s always fascinating how HTML and CSS are capable of handling the logic of an entire online game, so I had to try it! Such games usually rely on the ol’ Checkbox Hack where we combine the checked/unchecked state of a HTML input with the :checked pseudo-class in CSS. We can do a lot of magic with that one combination!

In fact, I challenged myself to build an entire game without Checkbox. I wasn’t sure if it would be possible, but it definitely is, and I’m going to show you how.

In addition to the puzzle game we will study in this article, I have made a collection of pure CSS games, most of them without the Checkbox Hack. (They are also available on CodePen.)

Want to play before we start?

I personally prefer playing the game in full screen mode, but you can play it below or open it up over here.

Cool right? I know, it’s not the Best Puzzle Game You Ever Saw™ but it’s also not bad at all for something that only uses CSS and a few lines of HTML. You can easily adjust the size of the grid, change the number of cells to control the difficulty level, and use whatever image you want!

We’re going to remake that demo together, then put a little extra sparkle in it at the end for some kicks.

The drag and drop functionality

While the structure of the puzzle is fairly straightforward with CSS Grid, the ability to drag and drop puzzle pieces is a bit trickier. I had to relying on a combination of transitions, hover effects, and sibling selectors to get it done.

If you hover over the empty box in that demo, the image moves inside of it and stays there even if you move the cursor out of the box. The trick is to add a big transition duration and delay — so big that the image takes lots of time to return to its initial position.

img {
  transform: translate(200%);
  transition: 999s 999s; /* very slow move on mouseout */
}
.box:hover img {
  transform: translate(0);
  transition: 0s; /* instant move on hover */
}

Specifying only the transition-delay is enough, but using big values on both the delay and the duration decreases the chance that a player ever sees the image move back. If you wait for 999s + 999s — which is approximately 30 minutes — then you will see the image move. But you won’t, right? I mean, no one’s going to take that long between turns unless they walk away from the game. So, I consider this a good trick for switching between two states.

Did you notice that hovering the image also triggers the changes? That’s because the image is part of the box element, which is not good for us. We can fix this by adding pointer-events: none to the image but we won’t be able to drag it later.

That means we have to introduce another element inside the .box:

That extra div (we’re using a class of .a) will take the same area as the image (thanks to CSS Grid and grid-area: 1 / 1) and will be the element that triggers the hover effect. And that is where the sibling selector comes into play:

.a {
  grid-area: 1 / 1;
}
img {
  grid-area: 1 / 1;
  transform: translate(200%);
  transition: 999s 999s;
}
.a:hover + img {
  transform: translate(0);
  transition: 0s;
}

Hovering on the .a element moves the image, and since it is taking up all space inside the box, it’s like we are hovering over the box instead! Hovering the image is no longer a problem!

Let’s drag and drop our image inside the box and see the result:

Did you see that? You first grab the image and move it to the box, nothing fancy. But once you release the image you trigger the hover effect that moves the image, and then we simulate a drag and drop feature. If you release the mouse outside the box, nothing happens.

Hmm, your simulation isn’t perfect because we can also hover the box and get the same effect.

True and we will rectify this. We need to disable the hover effect and allow it only if we release the image inside the box. We will play with the dimension of our .a element to make that happen.

Now, hovering the box does nothing. But if you start dragging the image, the .a element appears, and once released inside the box, we can trigger the hover effect and move the image.

Let’s dissect the code:

.a {
  width: 0%;
  transition: 0s .2s; /* add a small delay to make sure we catch the hover effect */
}
.box:active .a { /* on :active increase the width */
  width: 100%;
  transition: 0s; /* instant change */
}
img {
  transform: translate(200%);
  transition: 999s 999s;
}
.a:hover + img {
  transform: translate(0);
  transition: 0s;
}

Clicking on the image fires the :active pseudo-class that makes the .a element full-width (it is initially equal to 0). The active state will remain active until we release the image. If we release the image inside the box, the .a element goes back to width: 0, but we will trigger the hover effect before it happens and the image will fall inside the box! If you release it outside the box, nothing happens.

There is a little quirk: clicking the empty box also moves the image and breaks our feature. Currently, :active is linked to the .box element, so clicking on it or any of its children will activate it; and by doing this, we end up showing the .a element and triggering the hover effect.

We can fix that by playing with pointer-events. It allows us to disable any interaction with the .box while maintaining the interactions with the child elements.

.box {
  pointer-events: none;
}
.box * {
  pointer-events: initial;
}

Now our drag and drop feature is perfect. Unless you can find how to hack it, the only way to move the image is to drag it and drop it inside the box.

Building the puzzle grid

Putting the puzzle together is going to feel easy peasy compared to what we just did for the drag and drop feature. We are going to rely on CSS grid and background tricks to create the puzzle.

Here’s our grid, written in Pug for convenience:

- let n = 4; /* number of columns/rows */
- let image = "https://picsum.photos/id/1015/800/800";

g(style=`--i:url(${image})`)
  - for(let i = 0; i < n*n; i++)
    z
      a
      b(draggable="true") 

The code may look strange but it compiles into plain HTML:

<g style="--i: url(https://picsum.photos/id/1015/800/800)">
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
  <!-- etc. -->
</g>

I bet you’re wondering what’s up with those tags. None of these elements have any special meaning — I just find that the code is much easier to write using <z> than a bunch of <div class="z"> or whatever.

This is how I’ve mapped them out:

  • <g> is our grid container that contains N*N <z> elements.
  • <z> represents our grid items. It plays the role of the .box element we saw in the previous section.
  • <a> triggers the hover effect.
  • <b> represents a portion of our image. We apply the draggable attribute on it because it cannot be dragged by default.

Alright, let’s register our grid container on <g>. This is in Sass instead of CSS:

$n : 4; /* number of columns/rows */

g {
  --s: 300px; /* size of the puzzle */

  display: grid;
  max-width: var(--s);
  border: 1px solid;
  margin: auto;
  grid-template-columns: repeat($n, 1fr);
}

We’re actually going to make our grid children — the <z> elements — grids as well and have both <a> and <b> within the same grid area:

z {
  aspect-ratio: 1;
  display: grid;
  outline: 1px dashed;
}
a {
  grid-area: 1/1;
}
b {
  grid-area: 1/1;
}

As you can see, nothing fancy — we created a grid with a specific size. The rest of the CSS we need is for the drag and drop feature, which requires us to randomly place the pieces around the board. I’m going to turn to Sass for this, again for the convenience of being able to loop through and style all the puzzle pieces with a function:

b {
  background: var(--i) 0/var(--s) var(--s);
}

@for $i from 1 to ($n * $n + 1) {
  $r: (random(180));
  $x: (($i - 1)%$n);
  $y: floor(($i - 0.001) / $n);
  z:nth-of-type(#{$i}) b{
    background-position: ($x / ($n - 1)) * 100% ($y / ($n - 1)) * 100%;
    transform: 
      translate((($n - 1) / 2 - $x) * 100%, (($n - 1)/2 - $y) * 100%) 
      rotate($r * 1deg) 
      translate((random(100)*1% + ($n - 1) * 100%)) 
      rotate((random(20) - 10 - $r) * 1deg)
   }
}

You may have noticed that I’m using the Sass random() function. That’s how we get the randomized positions for the puzzle pieces. Remember that we will disable that position when hovering over the <a> element after dragging and dropping its corresponding <b> element inside the grid cell.

z a:hover ~ b {
  transform: translate(0);
  transition: 0s;
}

In that same loop, I am also defining the background configuration for each piece of the puzzle. All of them will logically share the same image as the background, and its size should be equal to the size of the whole grid (defined with the --s variable). Using the same background-image and some math, we update the background-position to show only a piece of the image.

That’s it! Our CSS-only puzzle game is technically done!

But we can always do better, right? I showed you how to make a grid of puzzle piece shapes in another article. Let’s take that same idea and apply it here, shall we?

Puzzle piece shapes

Here’s our new puzzle game. Same functionality but with more realistic shapes!

This is an illustration of the shapes on the grid:

If you look closely you’ll notice that we have nine different puzzle-piece shapes: the four corners, the four edges, and one for everything else.

The grid of puzzle pieces I made in the other article I referred to is a little more straightforward:

We can use the same technique that combines CSS masks and gradients to create the different shapes. In case you are unfamiliar with mask and gradients, I highly recommend checking that simplified case to better understand the technique before moving to the next part.

First, we need to use specific selectors to target each group of elements that shares the same shape. We have nine groups, so we will use eight selectors, plus a default selector that selects all of them.

z  /* 0 */

z:first-child  /* 1 */

z:nth-child(-n + 4):not(:first-child) /* 2 */

z:nth-child(5) /* 3 */

z:nth-child(5n + 1):not(:first-child):not(:nth-last-child(5)) /* 4 */

z:nth-last-child(5)  /* 5 */

z:nth-child(5n):not(:nth-child(5)):not(:last-child) /* 6 */

z:last-child /* 7 */

z:nth-last-child(-n + 4):not(:last-child) /* 8 */

Here is a figure that shows how that maps to our grid:

Now let’s tackle the shapes. Let’s focus on learning just one or two of the shapes because they all use the same technique — and that way, you have some homework to keep learning!

For the puzzle pieces in the center of the grid, 0:

mask: 
  radial-gradient(var(--r) at calc(50% - var(--r) / 2) 0, #0000 98%, #000) var(--r)  
    0 / 100% var(--r) no-repeat,
  radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% - var(--r) / 2), #0000 98%, #000) 
    var(--r) 50% / 100% calc(100% - 2 * var(--r)) no-repeat,
  radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
  radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);

The code may look complex, but let’s focus on one gradient at a time to see what’s happening:

Two gradients create two circles (marked green and purple in the demo), and two other gradients create the slots that other pieces connect to (the one marked blue fills up most of the shape while the one marked red fills the top portion). A CSS variable, --r, sets the radius of the circular shapes.

The shape of the puzzle pieces in the center (marked 0 in the illustration) is the hardest to make as it uses four gradients and has four curvatures. All the others pieces juggle fewer gradients.

For example, the puzzle pieces along the top edge of the puzzle (marked 2 in the illustration) uses three gradients instead of four:

mask: 
  radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% + var(--r) / 2), #0000 98%, #000) var(--r) calc(-1 * var(--r)) no-repeat,
  radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
  radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);

We removed the first (top) gradient and adjusted the values of the second gradient so that it covers the space left behind. You won’t notice a big difference in the code if you compare the two examples. It should be noted that we can find different background configurations to create the same shape. If you start playing with gradients you will for sure come up with something different than what I did. You may even write something that’s more concise — if so, share it in the comments!

In addition to creating the shapes, you will also find that I am increasing the width and/or the height of the elements like below:

height: calc(100% + var(--r));
width: calc(100% + var(--r));

The pieces of the puzzle need to overflow their grid cell to connect.

Final demo

Here is the full demo again. If you compare it with the first version you will see the same code structure to create the grid and the drag-and-drop feature, plus the code to create the shapes.

Possible enhancements

The article ends here but we could keep enhancing our puzzle with even more features! How about a a timer? Or maybe some sort of congratulations when the player finishes the puzzle?

I may consider all these features in a future version, so keep an eye on my GitHub repo.

Wrapping up

And CSS isn’t a programming language, they say. Ha!

I’m not trying to spark some #HotDrama by that. I say it because we did some really tricky logic stuff and covered a lot of CSS properties and techniques along the way. We played with CSS Grid, transitions, masking, gradients, selectors, and background properties. Not to mention the few Sass tricks we used to make our code easy to adjust.

The goal was not to build the game, but to explore CSS and discover new properties and tricks that you can use in other projects. Creating an online game in CSS is a challenge that pushes you to explore CSS features in great detail and learn how to use them. Plus, it’s just a lot of fun that we get something to play with when all is said and done.

Whether CSS is a programming language or not, doesn’t change the fact that we always learn by building and creating innovative stuff.


How I Made a Pure CSS Puzzle Game originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/how-i-made-a-pure-css-puzzle-game/feed/ 9 372952
Using Grid Named Areas to Visualize (and Reference) Your Layout https://css-tricks.com/using-grid-named-areas-to-visualize-and-reference-your-layout/ https://css-tricks.com/using-grid-named-areas-to-visualize-and-reference-your-layout/#comments Fri, 26 Aug 2022 13:44:49 +0000 https://css-tricks.com/?p=372634 Whenever we build simple or complex layouts using CSS Grid, we’re usually positioning items with line numbers. Grid layouts contain grid lines that are automatically indexed with positive and negative line numbers (that is unless we explicitly name them). …


Using Grid Named Areas to Visualize (and Reference) Your Layout originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Whenever we build simple or complex layouts using CSS Grid, we’re usually positioning items with line numbers. Grid layouts contain grid lines that are automatically indexed with positive and negative line numbers (that is unless we explicitly name them). Positioning items with line numbers is a fine way to lay things out, though CSS Grid has numerous ways to accomplish the same with an undersized cognitive encumbrance. One of those ways is something I like to think of as the “ASCII” method.

The ASCII method in a nutshell

The method boils down to using grid-template-areas to position grid items using custom-named areas at the grid container level rather than line numbers.

When we declare an element as a grid container using display: grid, the grid container, by default, generates a single-column track and rows that sufficiently hold the grid items. The container’s child elements that participate in the grid layout are converted to grid items, irrespective of their display property.

For instance, let’s create a grid by explicitly defining columns and rows using the grid-template-columns and grid-template-rows properties.

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

This little snippet of CSS creates a 3×2 grid where the grid items take up equal space in the columns, and where the grid contains three rows with a track size of 200px.

We can define the entire layout with named grid areas using the grid-template-areas property. According to the spec, the initial value of grid-template-areas is none.

grid-template-areas = none | <string>+

<string>+ is listing the group of strings enclosed with a quote. Each string is represented as a cell, and each quoted string is represented as a row. Like this:

grid-template-areas: "head head" "nav main" "foot foot";

The value of grid-template-areas describes the layout as having four grid areas. They are,

  • head
  • nav
  • main
  • foot

head and foot span two column tracks and one row track. The remaining nav and main each span one column track and one row track. The value of grid-template-areas is a lot like arranging ASCII characters, and as Chris suggested a while back, we can get a visualization of the overall structure of the layout from the CSS itself which is the most trouble-free way to understand it.

(Full size GIF)

OK, so we created our layout with four named grid areas: head, nav, main, foot.

Now, let’s start to position the grid items against named grid areas instead of line numbers. Specifically, let’s place a header element into the named grid area head and specify the named grid area head in the header element using the grid-area property.

Named grid areas in a grid layout are called idents. So, what we just did was create a custom ident named head that we can use to place items into certain grid tracks.

header { grid-area: head; }

We can other HTML elements using other custom idents:

nav { grid-area: nav; }
main { grid-area: main; }
footer { grid-area: foot; }

Writing named area values

According to CSS Grid Layout Module Level 1, all strings must be defined under the following tokens:

  • Named cell token: This represents the named grid area in the grid. For instance, head is a named cell token.
  • Null cell token: This represents the unnamed grid area in the grid container. For instance, an empty cell in the grid is a null cell token.
  • Trash token: This is a syntax error, such as an invalid declaration. For instance, a disparate number of cells and rows compared to the number of grid items would make a declaration invalid.

In grid-template-area, every quoted string (the rows) must have the same number of cells and define the complete grid without ignoring any cell.

We can ignore a cell or leave it as an empty cell using the full-stop character (.)

.grid { 
  display: grid;
  grid-template-areas:
    "head head"
    "nav main"
    "foot .";
}

If that feels visually awkward or imbalanced to you, we can use multiple full-stop characters without any whitespaces separating them:

.grid {
  display: grid;
  grid-template-areas:
    "head head"
    "nav main"
    "foot ....";
}

A named cell token can span multiple grid cells, But those cells must form a rectangular layout. In other words, we’re unable to create “L” or “T”-shaped layouts, although the spec does hint at support for non-rectangular layouts with disconnected regions in the future.

ASCII is better than line-based placement

Line-based placement is where we use the grid-column and grid-row properties to position an element on the grid using grid line numbers that are automatically indexed by a number:

.grid-item {
  grid-column: 1 / 3; /* start at grid column line 1 and span to line 3 */
}

But grid item line numbers can change if our layout changes at a breakpoint. In those cases, it’s not like we can rely on the same line numbers we used at a specific breakpoint. This is where it takes extra cognitive encumbrance to understand the code.

That’s why I think an ASCII-based approach works best. We can redefine the layout for each breakpoint using grid-template-areas within the grid container, which offers a convenient visual for how the layout will look directly in the CSS — it’s like self-documented code!

.grid {
  grid-template-areas:
    "head head"
    "nav main"
    "foot ...."; /* much easier way to see the grid! */
}

.grid-item {
  grid-area: foot; /* much easier to place the item! */
}

We can actually see a grid’s line numbers and grid areas in DevTools. In Firefox, for example, go to the Layout panel. Then, under the Grid tab, locate the “Grid display settings” and enable the “Display line number” and “Display area names” options.

Enabling grid settings.

This ASCII approach using named areas requires a lot less effort to visualize and easily find the placement of elements.

Line-based placement versus ASCII Art placement.

Let’s look at the “universal” use case

Whenever I see a tutorial on named grid areas, the common example is generally some layout pattern containing header, main, sidebar, and footer areas. I like to think of this as the “universal” use case since it casts such a wide net.

The Holy Grail layout in rectangles.

It’s a great example to illustrate how grid-template-areas works, but a real-life implementation usually involves media queries set to change the layout at certain viewport widths. Rather than having to re-declare grid-area on each grid item at each breakpoint to re-position everything, we can use grid-template-areas to “respond” to the breakpoint instead — and get a nice visual of the layout at each breakpoint in the process!

Before defining the layout, let’s assign an ident to each element using the grid-area property as a base style.

header {
  grid-area: head;
}

.left-side {
  grid-area: left;
}

main {
  grid-area: main;
}

.right-side {
  grid-area: right;
}

footer {
  grid-area: foot;
}

Now, let’s define the layout again as a base style. We’re going with a mobile-first approach so that things will stack by default:

.grid-container {
  display: grid;
  grid-template-areas:
    "head"
    "left"
    "main"
    "right"
    "foot";
}

Each grid item is auto-sized in this configuration — which seems a little bit weird — so we can set min-height: 100vh on the grid container to give us more room to work with:

.grid-container {
  display: grid;
  grid-template-areas:
    "head"
    "left"
    "main"
    "right"
    "foot";
  min-height: 100vh;
}

Now let’s say we want the main element to sit to the right of the stacked left and right sidebars when we get to a slightly wider viewport width. We re-declare grid-template-areas with an updated ASCII layout to get that:

@media (min-width: 800px) {
  .parent {
    grid-template-columns: 0.5fr 1fr;
    grid-template-rows: 100px 1fr 1fr 100px;
    grid-template-areas:
      "head head"
      "left main"
      "right main"
      "foot foot";
  }
}

I tossed some column and row sizing in there purely for aesthetics.

As the browser gets even wider, we may want to change the layout again, so that main is sandwiched between the left and right sidebars. Let’s write the layout visually!

.grid-container {
  grid-template-columns: 200px 1fr 200px; /* again, just for sizing */
  grid-template-areas:
    "head head head"
    "left main right"
    "left main right"
    "foot foot foot";
}

Leveraging implicit line names for flexibility

According to the spec, grid-template-areas automatically generates names for the grid lines created by named grid areas. We call these implicitly-named grid lines because they are named for us for free without any additional work.

Every named grid area gets four implicitly-named grid lines, two in the column direction and two in the row direction, where -start and -end are appended to the ident. For example, a grid area named head gets head-start and head-end lines in both directions for a total of four implicitly-named grid lines.

Implicitly assigned line names.

We can use these lines to our advantage! For instance, if we want an element to overlay the main, left, and right areas of our grid. Earlier, we talked about how layouts have to be rectangular — no “T” and “L” shaped layouts allowed. Consequently, we’re unable to use the ASCII visual layout method to place the overlay. We can, however, use our implicit line names using the same grid-area property on the overlay that we use to position the other elements.

Did you know that grid-area is a shorthand property, sort of the same way that margin and padding are shorthand properties? It takes multiple values the same way, but instead of following a “clockwise” direction like, margin — which goes in order of margin-block-start, margin-inline-end, margin-block-end, and margin-inline-startgrid-area goes like this:

grid-area: block-start / inline-start / block-end / inline-end;
Showing the block and inline flow directions in a left-to-right writing mode.

But we’re talking about rows and columns, not block and inline directions, right? Well, they correspond to one another. The row axis corresponds to the block direction, and the column axis corresponds to the inline direction:

grid-area: grid-row-start / grid-column-start / grid-row-end / grid-column-end;
Block and inline axis.

Back to positioning that overlay element as a grid item in our layout. The grid-area property will be helpful to position the element using our implicitly-named grid lines:

.overlay {
  grid-area: left-start / left-start / right-end / main-end;
}

Creating a minimal grid system

When we focus on layouts like the “universal” use case we just saw, it’s tempting to think of grid areas in terms of one area per element. But it doesn’t have to work like that. We can repeat idents to reserve more space for them in the layout. We saw that when we repeated the head and foot idents in the last example:

.grid-container {
  grid-template-areas:
    "head head head"
    "left main right"
    "left main right"
    "foot foot foot";
}

Notice that main, left, and right are also repeated but in the block direction.

Let’s forget about full page layouts and use named grid areas on a component. Grid is just as good for component layouts as full pages!

Here’s a pretty standard hero component that sports a row of images followed by different blocks of text:

A row of weightlifting photos above a heading, blurb, then a row of three links.

The HTML is pretty simple:

<div class="hero">
  <div class="image">
    <img src="..." alt="" />
  </div>
  <div class="text">
    <!-- ... -->
  </div>
</div>

We could do this for a real fast stacked layout:

.hero {
  grid-template-areas:
    "image"
    "text";
}

But then we have to reach for some padding, max-width or whatever to get the text area narrower than the row of images. How about we expand our ASCII layout into a four-column grid instead by repeating our idents on both rows:

.hero {
  display: grid;
  grid-template-columns: repeat(4, 1fr); /* maintain equal sizing */
  grid-template-areas:
    "image image image image"
    "text  text  text  text";
}

Alright, now we can place our grid items into those named areas:

.hero .image {
  grid-area: image;
}

.hero .text {
  grid-area: text;
}

So far, so good — both rows take up the entire width. We can use that as our base layout for small screens.

Showing grid lines on the stacked mobile version of the page.

But maybe we want to introduce the narrower text when the viewport reaches a larger width. We can use what we know about the full-stop character to “skip” columns. Let’s have the text ident skip the first and last columns in this case.

@media (min-width: 800px) {
  main {
    grid-template-columns: repeat(6, 1fr); /* increase to six columns */
    grid-template-areas:
      "image image image image image image"
      "..... text  text  text  text  .....";
  }
}

Now we have the spacing we want:

Showing grid lines for a table-sized layout of the page.

If the layout needs additional tweaking at even larger breakpoints, we can add more columns and go from there:

.hero {
  grid-template-columns: repeat(8, 1fr);
  grid-template-areas:
    "image image image image image image image image"
    "..... text  text  text  text  text  text  .....";
}

Dev tool visualization:

Showing grid lines for a large table sized layout of the page.

Remember when 12-column and 16-column layouts were the big things in CSS frameworks? We can quickly scale up to that and maintain a nice visual ASCII layout in the code:

main {
  grid-template-columns: repeat(12, 1fr);
  grid-template-areas:
    "image image image image image image image image image image image image"
    "..... text  text  text  text  text  text  text  text  text  text  .....";
}

Let’s look at something more complex

We’ve looked at one fairly generic example and one relatively straightforward example. We can still get nice ASCII layout visualizations with more complex layouts.

Let’s work up to this:

Three images positioned around a fancy heading.

I’ve split this up into two elements in the HTML, a header and a main:

<header>
  <div class="logo"> ... </div>
  <div class="menu"> ... </div>
</header>
<main>
  <div class="image"> ... </div>
  <h2> ... </h2>
  <div class="image"> ... </div>
  <div class="image"> ... </div>
</main>

I think flexbox is more appropriate for the header since we can space its child elements out easily that way. So, no grid there:

header {
  display: flex;
  justify-content: space-between;
  /* etc. */
}

But grid is well-suited for the main element’s layout. Let’s define the layout and assign the idents to the corresponding elements that we need to position the .text and three .image elements. We’ll start with this as our baseline for small screens:

.grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-areas:
    "image1 image1 .....  image2"
    "texts  texts  texts  texts"
    ".....  image3 image3 .....";
}

You can already see where we’re going with this, right? The layout is visualized for us, and we can drop the grid items into place with the custom idents:

.image:nth-child(1) {
  grid-area: image1;
}

.image:nth-last-child(2) {
  grid-area: image2;
}

.image:nth-last-child(1) {
  grid-area: image3;
}

h2 {
  grid-area: texts;
}
Showing grid lines on a mobile layout of the page.

That’s our base layout, so let’s venture into a wider breakpoint:

@media (min-width: 800px) {
  .grid {
    grid-template-columns: repeat(8, 1fr);
    grid-template-areas:
      ". image1 image1 ...... ......  ...... image2 ."
      ". texts  texts  texts  texts   texts  image2 ."
      ". .....  image3 image3 image3  image3 ...... .";
  }
}

I bet you know exactly how that will look because the layout is right there in the code!

Showing grid lines for a table-sized layout of the page.

Same deal if we decide to scale up even further:

.grid {
  grid-template-columns: repeat(12, 1fr);
  grid-template-areas:
    ". image1 image1 .....  .....   .....  .....  .....  .....  .....  .....  ."
    ". texts  texts  texts  texts   texts  texts  texts  texts  texts  image2 ."
    ". .....  image3 image3 image3  image3 .....  .....  .....  .....  .....  .";
}
Showing grid lines for a desktop-sized layout of the page.

Here’s the full demo:

I’m using the “negative margin hack” to get the first image to overlap the heading.

Wrapping up

I’m curious if anyone else is using grid-template-areas to create named areas for the benefit of having an ASCII visual of the grid layout. Having that as a reference in my CSS code has helped de-mystify some otherwise complex designs that may have been even more complex when dealing with line numbers.

But if nothing else, defining grid layouts this way teaches us some interesting things about CSS Grid that we saw throughout this post:

  • The grid-template-areas property allows us to create custom idents — or “named areas” — and use them to position grid items using the grid-area property.
  • There are three types of “tokens” that grid-template-areas accepts as values, including named cell tokens, null cell tokens, and trash cell tokens.
  • Each row that is defined in grid-template-areas needs the same number of cells. Ignoring a single cell doesn’t create a layout; it is considered a trash token.
  • We can get a visual ASCII-like diagram of the grid layout in the grid-template-areas property value by using required whitespaces between named cell tokens while defining the grid layout.
  • Make sure there is no whitespace inside a null cell token (e.g. .....). Otherwise, a single whitespace between null cell tokens creates unnecessary empty cells, resulting in an invalid layout.
  • We can redefine the layout at various breakpoints by re-positioning the grid items using grid-area, then re-declaring the layout with grid-template-areas on the grid container to update the track listing, if needed. There’s no need to touch the grid items.
  • Custom named grid areas automatically get four implicitly assigned line names — <custom-ident>-start and <custom-ident>-end in both the column and row directions.

Using Grid Named Areas to Visualize (and Reference) Your Layout originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/using-grid-named-areas-to-visualize-and-reference-your-layout/feed/ 5 372634
CSS Grid and Custom Shapes, Part 2 https://css-tricks.com/css-grid-and-custom-shapes-part-2/ https://css-tricks.com/css-grid-and-custom-shapes-part-2/#comments Mon, 22 Aug 2022 14:08:39 +0000 https://css-tricks.com/?p=372396 Alright, so the last time we checked in, we were using CSS Grid and combining them with CSS clip-path and mask techniques to create grids with fancy shapes.

Here’s just one of the fantastic grids we made together:

CodePen…


CSS Grid and Custom Shapes, Part 2 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Alright, so the last time we checked in, we were using CSS Grid and combining them with CSS clip-path and mask techniques to create grids with fancy shapes.

Here’s just one of the fantastic grids we made together:

CSS Grid and Custom Shapes series

Ready for the second round? We are still working with CSS Grid, clip-path, and mask, but by the end of this article, we’ll end up with different ways to arrange images on the grid, including some rad hover effects that make for an authentic, interactive experience to view pictures.

And guess what? We’re using the same markup that we used last time. Here’s that again:

<div class="gallery">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <!-- as many times as we want -->
</div>

Like the previous article, we only need a container with images inside. Nothing more!

Nested Image Grid

Last time, our grids were, well, typical image grids. Other than the neat shapes we masked them with, they were pretty standard symmetrical grids as far as how we positioned the images inside.

Let’s try nesting an image in the center of the grid:

We start by setting a 2✕2 grid for four images:

.gallery {
  --s: 200px; /* controls the image size */
  --g: 10px; /* controls the gap between images */

  display: grid;
  gap: var(--g);
  grid-template-columns: repeat(2, auto);
}
.gallery > img {
  width: var(--s);
  aspect-ratio: 1;
  object-fit: cover;
}

Nothing complex yet. The next step is to cut the corner of our images to create the space for the nested image. I already have a detailed article on how to cut corners using clip-path and mask. You can also use my online generator to get the CSS for masking corners.

What we need here is to cut out the corners at an angle equal to 90deg. We can use the same conic-gradient technique from that article to do that:

.gallery > img {
   mask: conic-gradient(from var(--_a), #0000 90deg, #000 0);
}
.gallery > img:nth-child(1) { --_a: 90deg; }
.gallery > img:nth-child(2) { --_a: 180deg; }
.gallery > img:nth-child(3) { --_a: 0deg; }
.gallery > img:nth-child(4) { --_a:-90deg; }

We could use the clip-path method for cutting corners from that same article, but masking with gradients is more suitable here because we have the same configuration for all the images — all we need is a rotation (defined with the variable --_a) get the effect, so we’re masking from the inside instead of the outside edges.

Two by two grid of images with a white square stacked on top in the center.

Now we can place the nested image inside the masked space. First, let’s make sure we have a fifth image element in the HTML:

<div class="gallery">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
</div>

We are going to rely on the good ol’ absolute positioning to place it in there:

.gallery > img:nth-child(5) {
  position: absolute;
  inset: calc(50% - .5*var(--s));
  clip-path: inset(calc(var(--g) / 4));
}

The inset property allows us to place the image at the center using a single declaration. We know the size of the image (defined with the variable --s), and we know that the container’s size equals 100%. We do some math, and the distance from each edge should be equal to (100% - var(--s))/2.

Diagram of the widths needed to complete the design.

You might be wondering why we’re using clip-path at all here. We’re using it with the nested image to have a consistent gap. If we were to remove it, you would notice that we don’t have the same gap between all the images. This way, we’re cutting a little bit from the fifth image to get the proper spacing around it.

The complete code again:

.gallery {
  --s: 200px; /* controls the image size */
  --g: 10px;  /* controls the gap between images */
  
  display: grid;
  gap: var(--g);
  grid-template-columns: repeat(2, auto);
  position: relative;
}

.gallery > img {
  width: var(--s);
  aspect-ratio: 1;
  object-fit: cover;
  mask: conic-gradient(from var(--_a), #0000 90deg, #000 0);
}

.gallery > img:nth-child(1) {--_a: 90deg}
.gallery > img:nth-child(2) {--_a:180deg}
.gallery > img:nth-child(3) {--_a:  0deg}
.gallery > img:nth-child(4) {--_a:-90deg}
.gallery > img:nth-child(5) {
  position: absolute;
  inset: calc(50% - .5*var(--s));
  clip-path: inset(calc(var(--g) / 4));
}

Now, many of you might also be wondering: why all the complex stuff when we can place the last image on the top and add a border to it? That would hide the images underneath the nested image without a mask, right?

That’s true, and we will get the following:

No mask, no clip-path. Yes, the code is easy to understand, but there is a little drawback: the border color needs to be the same as the main background to make the illusion perfect. This little drawback is enough for me to make the code more complex in exchange for real transparency independent of the background. I am not saying a border approach is bad or wrong. I would recommend it in most cases where the background is known. But we are here to explore new stuff and, most important, build components that don’t depend on their environment.

Let’s try another shape this time:

This time, we made the nested image a circle instead of a square. That’s an easy task with border-radius But we need to use a circular cut-out for the other images. This time, though, we will rely on a radial-gradient() instead of a conic-gradient() to get that nice rounded look.

.gallery > img {
  mask: 
    radial-gradient(farthest-side at var(--_a),
      #0000 calc(50% + var(--g)/2), #000 calc(51% + var(--g)/2));
}
.gallery > img:nth-child(1) { --_a: calc(100% + var(--g)/2) calc(100% + var(--g)/2); }
.gallery > img:nth-child(2) { --_a: calc(0%   - var(--g)/2) calc(100% + var(--g)/2); }
.gallery > img:nth-child(3) { --_a: calc(100% + var(--g)/2) calc(0%   - var(--g)/2); }
.gallery > img:nth-child(4) { --_a: calc(0%   - var(--g)/2) calc(0%   - var(--g)/2); }

All the images use the same configuration as the previous example, but we update the center point each time.

Diagram showing the center values for each quadrant of the grid.

The above figure illustrates the center point for each circle. Still, in the actual code, you will notice that I am also accounting for the gap to ensure all the points are at the same position (the center of the grid) to get a continuous circle if we combine them.

Now that we have our layout let’s talk about the hover effect. In case you didn’t notice, a cool hover effect increases the size of the nested image and adjusts everything else accordingly. Increasing the size is a relatively easy task, but updating the gradient is more complicated since, by default, gradients cannot be animated. To overcome this, I will use a font-size hack to be able to animate the radial gradient.

If you check the code of the gradient, you can see that I am adding 1em:

mask: 
    radial-gradient(farthest-side at var(--_a),
      #0000 calc(50% + var(--g)/2 + 1em), #000 calc(51% + var(--g)/2 + 1em));

It’s known that em units are relative to the parent element’s font-size, so changing the font-size of the .gallery will also change the computed em value — this is the trick we are using. We are animating the font-size from a value of 0 to a given value and, as a result, the gradient is animated, making the cut-out part larger, following the size of the nested image that is getting bigger.

Here is the code that highlights the parts involved in the hover effect:

.gallery {
  --s: 200px; /* controls the image size */
  --g: 10px; /* controls the gaps between images */

  font-size: 0; /* initially we have 1em = 0 */
  transition: .5s;
}
/* we increase the cut-out by 1em */
.gallery > img {
  mask: 
    radial-gradient(farthest-side at var(--_a),
      #0000 calc(50% + var(--g)/2 + 1em), #000 calc(51% + var(--g)/2 + 1em));
}
/* we increase the size by 2em */
.gallery > img:nth-child(5) {
  width: calc(var(--s) + 2em);
}
/* on hover 1em = S/5 */
.gallery:hover {
  font-size: calc(var(--s) / 5);
}

The font-size trick is helpful if we want to animate gradients or other properties that cannot be animated. Custom properties defined with @property can solve such a problem, but support for it is still lacking at the time of writing.

I discovered the font-size trick from @SelenIT2 while trying to solve a challenge on Twitter.

Another shape? Let’s go!

This time we clipped the nested image into the shape of a rhombus. I’ll let you dissect the code as an exercise to figure out how we got here. You will notice that the structure is the same as in our examples. The only differences are how we’re using the gradient to create the shape. Dig in and learn!

Circular Image Grid

We can combine what we’ve learned here and in previous articles to make an even more exciting image grid. This time, let’s make all the images in our grid circular and, on hover, expand an image to reveal the entire thing as it covers the rest of the photos.

The HTML and CSS structure of the grid is nothing new from before, so let’s skip that part and focus instead on the circular shape and hover effect we want.

We are going to use clip-path and its circle() function to — you guessed it! — cut a circle out of the images.

Showing the two states of an image, the natural state on the left, and the hovered state on the right, including the clip-path values to create them.

That figure illustrates the clip-path used for the first image. The left side shows the image’s initial state, while the right shows the hovered state. You can use this online tool to play and visualize the clip-path values.

For the other images, we can update the center of the circle (70% 70%) to get the following code:

.gallery > img:hover {
  --_c: 50%; /* same as "50% at 50% 50%" */
}
.gallery > img:nth-child(1) {
  clip-path: circle(var(--_c, 55% at 70% 70%));
}
.gallery > img:nth-child(2) {
  clip-path: circle(var(--_c, 55% at 30% 70%));
}
.gallery > img:nth-child(3) {
  clip-path: circle(var(--_c, 55% at 70% 30%));
}
.gallery > img:nth-child(4) {
  clip-path: circle(var(--_c, 55% at 30% 30%));
}

Note how we are defining the clip-path values as a fallback inside var(). This way allows us to more easily update the value on hover by setting the value of the --_c variable. When using circle(), the default position of the center point is 50% 50%, so we get to omit that for more concise code. That’s why you see that we are only setting 50% instead of 50% at 50% 50%.

Then we increase the size of our image on hover to the overall size of the grid so we can cover the other images. We also ensure the z-index has a higher value on the hovered image, so it is the top one in our stacking context.

.gallery {
  --s: 200px; /* controls the image size */
  --g: 8px;   /* controls the gap between images */

  display: grid;
  grid: auto-flow var(--s) / repeat(2, var(--s));
  gap: var(--g);
}

.gallery > img {
  width: 100%; 
  aspect-ratio: 1;
  cursor: pointer;
  z-index: 0;
  transition: .25s, z-index 0s .25s;
}
.gallery > img:hover {
  --_c: 50%; /* change the center point on hover */
  width: calc(200% + var(--g));
  z-index: 1;
  transition: .4s, z-index 0s;
}

.gallery > img:nth-child(1){
  clip-path: circle(var(--_c, 55% at 70% 70%));
  place-self: start;
}
.gallery > img:nth-child(2){
  clip-path: circle(var(--_c, 55% at 30% 70%));
  place-self: start end;
}
.gallery > img:nth-child(3){
  clip-path: circle(var(--_c, 55% at 70% 30%));
  place-self: end start;
}
.gallery > img:nth-child(4){
  clip-path: circle(var(--_c, 55% at 30% 30%));
  place-self: end;
}

What’s going on with the place-self property? Why do we need it and why does each image have a specific value?

Do you remember the issue we had in the previous article when creating the grid of puzzle pieces? We increased the size of the images to create an overflow, but the overflow of some images was incorrect. We fixed them using the place-self property.

Same issue here. We are increasing the size of the images so each one overflows its grid cells. But if we do nothing, all of them will overflow on the right and bottom sides of the grid. What we need is:

  1. the first image to overflow the bottom-right edge (the default behavior),
  2. the second image to overflow the bottom-left edge,
  3. the third image to overflow the top-right edge, and
  4. the fourth image to overflow the top-left edge.

To get that, we need to place each image correctly using the place-self property.

Diagram showing the place-self property values for each quadrant of the grid.

In case you are not familiar with place-self, it’s the shorthand for justify-self and align-self to place the element horizontally and vertically. When it takes one value, both alignments use that same value.

Expanding Image Panels

In a previous article, I created a cool zoom effect that applies to a grid of images where we can control everything: number of rows, number of columns, sizes, scale factor, etc.

A particular case was the classic expanding panels, where we only have one row and a full-width container.

We will take this example and combine it with shapes!

Before we continue, I highly recommend reading my other article to understand how the tricks we’re about to cover work. Check that out, and we’ll continue here to focus on creating the panel shapes.

First, let’s start by simplifying the code and removing some variables

We only need one row and the number of columns should adjust based on the number of images. That means we no longer need variables for the number of rows (--n) and columns (--m ) but we need to use grid-auto-flow: column, allowing the grid to auto-generate columns as we add new images. We will consider a fixed height for our container; by default, it will be full-width.

Let’s clip the images into a slanted shape:

A headshot of a calm red wolf looking downward with vertices overlayed showing the clip-path property points.
clip-path: polygon(S 0%, 100% 0%, (100% - S) 100%, 0% 100%);

Once again, each image is contained in its grid cell, so there’s more space between the images than we’d like:

A six-panel grid of slanted images of various wild animals showing the grid lines and gaps.

We need to increase the width of the images to create an overlap. We replace min-width: 100% with min-width: calc(100% + var(--s)), where --s is a new variable that controls the shape.

Now we need to fix the first and last images, so they sort of bleed off the page without gaps. In other words, we can remove the slant from the left side of the first image and the slant from the right side of the last image. We need a new clip-path specifically for those two images.

We also need to rectify the overflow. By default, all the images will overflow on both sides, but for the first one, we need an overflow on the right side while we need a left overflow for the last image.

.gallery > img:first-child {
  min-width: calc(100% + var(--s)/2);
  place-self: start;
  clip-path: polygon(0 0,100% 0,calc(100% - var(--s)) 100%,0 100%);
}
.gallery > img:last-child {
  min-width: calc(100% + var(--s)/2);
  place-self: end;
  clip-path: polygon(var(--s) 0,100% 0,100% 100%,0 100%);
}

The final result is a nice expanding panel of slanted images!

We can add as many images as you want, and the grid will adjust automatically. Plus, we only need to control one value to control the shape!

We could have made this same layout with flexbox since we are dealing with a single row of elements. Here is my implementation.

Sure, slanted images are cool, but what about a zig-zag pattern? I already teased this one at the end of the last article.

All I’m doing here is replacing clip-path with mask… and guess what? I already have a detailed article on creating that zig-zag shape — not to mention an online generator to get the code. See how all everything comes together?

The trickiest part here is to make sure the zig-zags are perfectly aligned, and for this, we need to add an offset for every :nth-child(odd) image element.

.gallery > img {
  mask: 
    conic-gradient(from -135deg at right, #0000, #000 1deg 89deg, #0000 90deg) 
      100% calc(50% + var(--_p, 0%))/51% calc(2*var(--s)) repeat-y,
    conic-gradient(from   45deg at left,  #0000, #000 1deg 89deg, #0000 90deg) 
      0%   calc(50% + var(--_p, 0%))/51% calc(2*var(--s)) repeat-y;
}
/* we add an offset to the odd elements */
.gallery > img:nth-child(odd) {
  --_p: var(--s);
}
.gallery > img:first-child {
  mask: 
    conic-gradient(from -135deg at right, #0000, #000 1deg 89deg, #0000 90deg) 
      0 calc(50% + var(--_p, 0%))/100% calc(2*var(--s));
}
.gallery > img:last-child {
  mask: 
    conic-gradient(from 45deg at left, #0000, #000 1deg 89deg, #0000 90deg) 
      0 calc(50% + var(--_p, 0%)) /100% calc(2*var(--s));
}

Note the use of the --_p variable, which will fall back to 0% but will be equal to --_s for the odd images.

Here is a demo that illustrates the issue. Hover to see how the offset — defined by --_p — is fixing the alignment.

Also, notice how we use a different mask for the first and last image as we did in the previous example. We only need a zig-zag on the right side of the first image and the left side of the last image.

And why not rounded sides? Let’s do it!

I know that the code may look scary and tough to understand, but all that’s going on is a combination of different tricks we’ve covered in this and other articles I’ve already shared. In this case, I use the same code structure as the zig-zag and the slanted shapes. Compare it with those examples, and you will find no difference! Those are the same tricks in my previous article about the zoom effect. Then, I am using my other writing and my online generator to get the code for the mask that creates those rounded shapes.

If you recall what we did for the zig-zag, we had used the same mask for all the images but then had to add an offset to the odd images to create a perfect overlap. In this case, we need a different mask for the odd-numbered images.

The first mask:

mask: 
  linear-gradient(-90deg,#0000 calc(2*var(--s)),#000 0) var(--s),
  radial-gradient(var(--s),#000 98%,#0000) 50% / calc(2*var(--s)) calc(1.8*var(--s)) space repeat;

The second one:

mask:
  radial-gradient(calc(var(--s) + var(--g)) at calc(var(--s) + var(--g)) 50%,#0000 98% ,#000) 
  calc(50% - var(--s) - var(--g)) / 100% calc(1.8*var(--s))

The only effort I did here is update the second mask to include the gap variable (--g) to create that space between the images.

The final touch is to fix the first and last image. Like all the previous examples, the first image needs a straight left edge while the last one needs a straight right edge.

For the first image, we always know the mask it needs to have, which is the following:

.gallery > img:first-child {
  mask: 
    radial-gradient(calc(var(--s) + var(--g)) at right, #0000 98%, #000) 50% / 100% calc(1.8 * var(--s));
}
A brown bear headshot with a wavy pattern for the right border.

For the last image, it depends on the number of elements, so it matters if that element is :nth-child(odd) or :nth-child(even).

The complete grid of wild animal photos with all of the correct borders and gaps between images.
.gallery > img:last-child:nth-child(even) {
  mask: 
    linear-gradient(to right,#0000 var(--s),#000 0),
    radial-gradient(var(--s),#000 98%,#0000) left / calc(2*var(--s)) calc(1.8*var(--s)) repeat-y
}
A single-row grid of three wild animal photos with wavy borders where the last image is an odd-numbered element.
.gallery > img:last-child:nth-child(odd) {
  mask: 
    radial-gradient(calc(var(--s) + var(--g)) at left,#0000 98%,#000) 50% / 100% calc(1.8*var(--s))
}

That’s all! Three different layouts but the same CSS tricks each time:

  • the code structure to create the zoom effect
  • a mask or clip-path to create the shapes
  • a separate configuration for the odd elements in some cases to make sure we have a perfect overlap
  • a specific configuration for the first and last image to keep the shape on only one side.

And here is a big demo with all of them together. All you need is to add a class to activate the layout you want to see.

And here is the one with the Flexbox implementation

Wrapping up

Oof, we are done! I know there are many CSS tricks and examples between this article and the last one, not to mention all of the other tricks I’ve referenced here from other articles I’ve written. It took me time to put everything together, and you don’t have to understand everything at once. One reading will give you a good overview of all the layouts, but you may need to read the article more than once and focus on each example to grasp all the tricks.

Did you notice that we didn’t touch the HTML at all other than perhaps the number of images in the markup? All the layouts we made share the same HTML code, which is nothing but a list of images.

Before I end, I will leave you with one last example. It’s a “versus” between two anime characters with a cool hover effect.

What about you? Can you create something based on what you have learned? It doesn’t need to be complex — imagine something cool or funny like I did with that anime matchup. It can be a good exercise for you, and we may end with an excellent collection in the comment section.


CSS Grid and Custom Shapes, Part 2 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-grid-and-custom-shapes-part-2/feed/ 4 372396
CSS Grid and Custom Shapes, Part 1 https://css-tricks.com/css-grid-and-custom-shapes-part-1/ https://css-tricks.com/css-grid-and-custom-shapes-part-1/#comments Mon, 15 Aug 2022 13:13:47 +0000 https://css-tricks.com/?p=367324 In a previous article, I looked at CSS Grid’s ability to create complex layouts using its auto-placement powers. I took that one step further in another article that added a zooming hover effect to images in a grid layout


CSS Grid and Custom Shapes, Part 1 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In a previous article, I looked at CSS Grid’s ability to create complex layouts using its auto-placement powers. I took that one step further in another article that added a zooming hover effect to images in a grid layout. This time, I want to dive into another type of grid, one that works with shapes.

Like, what if the images aren’t perfectly square but instead are shaped like hexagons or rhombuses? Spoiler alert: we can do it. In fact, we’re going to combine CSS Grid techniques we’ve looked at and drop in some CSS clip-path and mask magic to create fancy grids of images for just about any shape you can imagine!

CSS Grid and Custom Shapes series

Let’s start with some markup

Most of the layouts we are going to look at may look easy to achieve at first glance, but the challenging part is to achieve them with the same HTML markup. We can use a lot of wrappers, divs, and whatnot, but the goal of this post is to use the same and smallest amount of HTML code and still get all the different grids we want. After all, what’s CSS but a way to separate styling and markup? Our styling should not depend on the markup, and vice versa.

This said, let’s start with this:

<div class="gallery">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <!-- as many times as we want -->
</div>

A container with images is all that we need here. Nothing more!

CSS Grid of Hexagons

This is also sometimes referred to as a “honeycomb” grid.

There are already plenty of other blog posts out there that show how to make this. Heck, I wrote one here on CSS-Tricks! That article is still good and goes way deep on making a responsive layout. But for this specific case, we are going to rely on a much simpler CSS approach.

First, let’s use clip-path on the images to create the hexagon shape and we place all of them in the same grid area so they overlap.

.gallery {
  --s: 150px; /* controls the size */
  display: grid;
}

.gallery > img {
  grid-area: 1/1;
  width: var(--s);
  aspect-ratio: 1.15;
  object-fit: cover;
  clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0 50%);
}
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0 50%)

Nothing fancy yet. All the images are hexagons and above each other. So it looks like all we have is a single hexagon-shaped image element, but there are really seven.

The next step is to apply a translation to the images to correctly place them on the grid.

Notice that we still want one of the images to remain in the center. The rest are placed around it using CSS translate and good ol’ fashioned geometry. Here’s are the mock formulas I came up with for each image in the grid:

translate((height + gap)*sin(0deg), (height + gap)*cos(0))
translate((height + gap)*sin(60deg), (height + gap)*cos(60deg))
translate((height + gap)*sin(120deg), (height + gap)*cos(120deg))
translate((height + gap)*sin(180deg), (height + gap)*cos(180deg))
translate((height + gap)*sin(240deg), (height + gap)*cos(240deg))
translate((height + gap)*sin(300deg), (height + gap)*cos(300deg))

A few calculations and optimization later (let’s skip that boring part, right?) we get the following CSS:

.gallery {
  --s: 150px; /* control the size */
  --g: 10px;  /* control the gap */
  display: grid;
}
.gallery > img {
  grid-area: 1/1;
  width: var(--s);
  aspect-ratio: 1.15;
  object-fit: cover;
  clip-path: polygon(25% 0%, 75% 0%, 100% 50% ,75% 100%, 25% 100%, 0 50%);
  transform: translate(var(--_x,0), var(--_y,0));
}
.gallery > img:nth-child(1) { --_y: calc(-100% - var(--g)); }
.gallery > img:nth-child(7) { --_y: calc( 100% + var(--g)); }
.gallery > img:nth-child(3),
.gallery > img:nth-child(5) { --_x: calc(-75% - .87*var(--g)); }
.gallery > img:nth-child(4),
.gallery > img:nth-child(6) { --_x: calc( 75% + .87*var(--g)); }
.gallery > img:nth-child(3),
.gallery > img:nth-child(4) { --_y: calc(-50% - .5*var(--g)); }
.gallery > img:nth-child(5), 
.gallery > img:nth-child(6) { --_y: calc( 50% + .5*var(--g)); }

Maybe that’ll be easier when we get real trigonometry functions in CSS!

Each image is translated by the --_x and --_y variables that are based on those formulas. Only the second image (nth-child(2)) is undefined in any selector because it’s the one in the center. It can be any image if you decide to use a different order. Here’s the order I’m using:

With only a few lines of code, we get a cool grid of images. To this, I added a little hover effect to the images to make things fancier.

Guess what? We can get another hexagon grid by simply updating a few values.

If you check the code and compare it with the previous one you will notice that I have simply swapped the values inside clip-path and I switched between --x and --y. That’s all!

CSS Grid of Rhombuses

Rhombus is such a fancy word for a square that’s rotated 45 degrees.

Same HTML, remember? We first start by defining a 2×2 grid of images in CSS:

.gallery {
  --s: 150px; /* controls the size */

  display: grid;
  gap: 10px;
  grid: auto-flow var(--s) / repeat(2, var(--s));
  place-items: center;
}
.gallery > img {
  width: 100%; 
  aspect-ratio: 1;
  object-fit: cover;
}

The first thing that might catch your eye is the grid property. It’s pretty uncommonly used but is super helpful in that it’s a shorthand that lets you define a complete grid in one declaration. It’s not the most intuitive — and not to mention readable — property, but we are here to learn and discover new things, so let’s use it rather than writing out all of the individual grid properties.

grid: auto-flow var(--s) / repeat(2,var(--s));

/* is equivalent to this: */
grid-template-columns: repeat(2, var(--s));
grid-auto-rows: var(--s);

This defines two columns equal to the --s variable and sets the height of all the rows to --s as well. Since we have four images, we will automatically get a 2×2 grid.

Here’s another way we could have written it:

grid-template-columns: repeat(2, var(--s));
grid-template-rows: repeat(2, var(--s));

…which can be reduced with the grid shorthand:

grid: repeat(2,var(--s)) / repeat(2,var(--s));

After setting the grid, we rotate it and the images with CSS transforms and we get this:

Note how I rotate them both by 45deg, but in the opposite direction.

.gallery {
  /* etc. */
  transform: rotate(45deg);
}
.gallery > img {
  /* etc. */
  transform: rotate(-45deg);
}

Rotating the images in the negative direction prevents them from getting rotated with the grid so they stay straight. Now, we apply a clip-path to clip a rhombus shape out of them.

clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%)

We are almost done! We need to rectify the size of the image to make them fit together. Otherwise, they’re spaced far apart to the point where it doesn’t look like a grid of images.

The image is within the boundary of the green circle, which is the inscribed circle of the grid area where the image is placed. What we want is to make the image bigger to fit inside the red circle, which is the circumscribed circle of the grid area.

Don’t worry, I won’t introduce any more boring geometry. All you need to know is that the relationship between the radius of each circle is the square root of 2 (sqrt(2)). This is the value we need to increase the size of our images to fill the area. We will use 100%*sqrt(2) = 141% and be done!

.gallery {
  --s: 150px; /* control the size */

  display: grid;
  grid: auto-flow var(--s) / repeat(2,var(--s));
  gap: 10px;
  place-items: center;
  transform: rotate(45deg);
}
.gallery > img {
  width: 141%; /* 100%*sqrt(2) = 141% */
  aspect-ratio: 1;
  object-fit: cover;
  transform: rotate(-45deg);
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

Like the hexagon grid, we can make things fancier with that nice zooming hover effect:

CSS Grid of Triangular Shapes

You probably know by now that the big trick is figuring out the clip-path to get the shapes we want. For this grid, each element has its own clip-path value whereas the last two grids worked with a consistent shape. So, this time around, it’s like we’re working with a few different triangular shapes that come together to form a rectangular grid of images.

The three images at the top
The three images at the bottom

We place them inside a 3×2 grid with the following CSS:

.gallery {
  display: grid;
  gap: 10px; 
  grid-template-columns: auto auto auto; /* 3 columns */
  place-items: center;
}
.gallery > img {
  width: 200px; /* controls the size */
  aspect-ratio: 1;
  object-fit: cover;
}
/* the clip-path values */
.gallery > img:nth-child(1) { clip-path: polygon(0 0, 50% 0, 100% 100% ,0 100%); }
.gallery > img:nth-child(2) { clip-path: polygon(0 0, 100% 0, 50% 100%); }
.gallery > img:nth-child(3) { clip-path: polygon(50% 0, 100% 0, 100% 100%, 0 100%); }
.gallery > img:nth-child(4) { clip-path: polygon(0 0, 100% 0, 50% 100%, 0 100%); }
.gallery > img:nth-child(5) { clip-path: polygon(50% 0, 100% 100%, 0% 100%); }
.gallery > img:nth-child(6) { clip-path: polygon(0 0, 100% 0 ,100% 100%, 50% 100%); } }

Here’s what we get:

The final touch is to make the width of the middle column equal 0 to get rid of the spaces between the images. The same sort of spacing problem we had with the rhombus grid, but with a different approach for the shapes we’re using:

grid-template-columns: auto 0 auto;

I had to fiddle with the clip-path values to make sure they would all appear to fit together nicely like a puzzle. The original images overlap when the middle column has zero width, but after slicing the images, the illusion is perfect:

CSS Pizza Pie Grid

Guess what? We can get another cool grid by simply adding border-radius and overflow to our grid or triangular shapes. 🎉

CSS Grid of Puzzle Pieces

This time we are going to play with the CSS mask property to make the images look like pieces of a puzzle.

If you haven’t used mask with CSS gradients, I highly recommend this other article I wrote on the topic because it’ll help with what comes next. Why gradients? Because that’s what we’re using to get the round notches in the puzzle piece shapes.

Setting up the grid should be a cinch by now, so let’s focus instead on the mask part.

As illustrated in the above demo, we need two gradients to create the final shape. One gradient creates a circle (the green part) and the other creates the right curve while filling in the top part.

--g: 6px; /* controls the gap */
--r: 42px;  /* control the circular shapes */

background: 
  radial-gradient(var(--r) at left 50% bottom var(--r), green 95%, #0000),
  radial-gradient(calc(var(--r) + var(--g)) at calc(100% + var(--g)) 50%, #0000 95%, red)
  top/100% calc(100% - var(--r)) no-repeat;

Two variables control the shape. The --g variable is nothing but the grid gap. We need to account for the gap to correctly place our circles so they overlap perfectly when the whole puzzle is assembled. The --r variable controls the size of circular parts of the puzzle shape.

Now we take the same CSS and update a few values in it to create the three other shapes:

We have the shapes, but not the overlapping edges we need to make them fit together. Each image is limited to the grid cell it’s in, so it makes sense why the shapes are sort of jumbled at the moment:

We need to create an overflow by increasing the height/width of the images. From the above figure, we have to increase the height of the first and fourth images while we increase the width of the second and third ones. You have probably already guessed that we need to increase them using the --r variable.

.gallery > img:is(:nth-child(1),:nth-child(4)) {
  width: 100%;
  height: calc(100% + var(--r));
}
.gallery > img:is(:nth-child(2),:nth-child(3)) {
  height: 100%;
  width: calc(100% + var(--r));
}

We are getting closer!

We created the overlap but, by default, our images either overlap on the right (if we increase the width) or the bottom (if we increase the height). But that’s not what we want for the second and fourth images. The fix is to use place-self: end on those two images and our full code becomes this:

Here is another example where I am using a conic gradient instead of a radial gradient. This gives us triangular puzzle pieces while keeping the same underlying HTML and CSS.

A last one! This time I am using clip-path and since it’s a property we can animate, we get a cool hover by simply updating the custom property that controls the shape.

Wrapping up

That’s all for this first part! By combining the things we’ve already learned about CSS Grid with some added clip-path and mask magic, we were able to make grid layouts featuring different kinds of shapes. And we used the same HTML markup each time! And the markup itself is nothing more than a container with a handful of image elements!

In the second part, we are going to explore more complex-looking grids with more fancy shapes and hover effects.

I’m planning to take the demo of expanding image panels we made together in this other article:

…and transform it into a zig-zag image panels! And this is only one example among the many we will discover in the next article.


CSS Grid and Custom Shapes, Part 1 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-grid-and-custom-shapes-part-1/feed/ 12 367324
Zooming Images in a Grid Layout https://css-tricks.com/zooming-images-in-a-grid-layout/ https://css-tricks.com/zooming-images-in-a-grid-layout/#comments Mon, 08 Aug 2022 12:55:49 +0000 https://css-tricks.com/?p=367204 Creating a grid of images is easy, thanks to CSS Grid. But making the grid do fancy things after the images have been placed can be tricky to pull off.

Say you want to add some fancy hover effect to …


Zooming Images in a Grid Layout originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Creating a grid of images is easy, thanks to CSS Grid. But making the grid do fancy things after the images have been placed can be tricky to pull off.

Say you want to add some fancy hover effect to the images where they grow and zoom beyond the rows and columns where they sit? We can do that!

Cool, right? If you check the code, you won’t find any JavaScript, complex selectors, or even magic numbers. And this is only one example among many we will explore!

Building the grid

The HTML code to create the grid is as simple as a list of images within a container. We don’t need more than that.

<div class="gallery">
  <img>
  <img>
  <img>
  <!-- etc. -->
</div>

For the CSS, we first start by setting the grid using the following:

.gallery {
  --s: 150px; /* controls the size */
  --g: 10px;  /* controls the gap */

  display: grid;
  gap: var(--g);
  width: calc(3*var(--s) + 2*var(--g)); /* 3 times the size plus 2 times the gap */
  aspect-ratio: 1;
  grid-template-columns: repeat(3, auto);
}

In short, we have two variables, one that controls the size of the images and one that sets the size of the gap between images. aspect-ratio helps keep things in proportion.

You might be wondering why we are only defining three columns but no rows. No, I didn’t forget the rows — we just don’t need to explicitly set them. CSS Grid is capable of automatically placing items on implicit rows and columns, meaning we get as many rows as needed to any number of images we throw at it. We can explicitly define the rows instead but we need to add grid-auto-flow: column to make sure the browser will create the needed columns for us.

Here is an example to illustrate both cases. The difference is that one flows in a row direction an the other in a column direction.

Check out this other article I wrote for more about the implicit grids and the auto-placement algorithm.

Now that we have our grid, it’s time to style the images:

.gallery > img {
  width: 0;
  height: 0;
  min-height: 100%;
  min-width: 100%;
  object-fit: cover;
}

The hover effect we’re making relies on this CSS. It probably looks weird to you that we’re making images that have both no width or height but have a minimum width and height of 100%. But you will see that it’s a pretty neat trick for what we are trying to achieve.

What I’m doing here is telling the browser that the images need to have 0 width and height but also need to have a minimum height equal to 100%… but 100% of what? When using percentages, the value is relative to something else. In this case, our image is placed inside a grid cell and we need to know that size to know what’s 100% is relative to.

The browser will first ignore min-height: 100% to calculate the size of the grid cells, but it will use the height: 0 in its calculation. That means our images will not contribute to the size of the grid cells… because they technically have no physical size. This will result in three equal columns and rows that are based on the size of the grid (which we defined on the .gallery’s width and aspect-ratio). The height of each grid cell is nothing but the variable --s we defined (same for the width).

Now that we have the dimensions of our grid’s cells, the browser will use it with min-height: 100% (and min-width: 100%) which will force the images to completely fill the space of each grid cell. The whole thing may look a bit confusing but the main idea is to make sure that the grid defines the size of the images rather than the other way around. I don’t want the image to define the size of the grid and you will understand why after adding the hover effect.

Creating the hover effect

What we need to do is increase the scale of the images when they’re hovered. We can do that by adjusting an image’s width and height on :hover:

.gallery {
  --f: 1.5; /* controls the scale factor */
}

.gallery img:hover{
  width:  calc(var(--s) * var(--f));
  height: calc(var(--s) * var(--f));
}

I added a new custom variable, --f, to the mix as a scale factor to control the size on hover. Notice how I’m multiplying the size variable, --s, by it to calculate the new image size.

But you said that the image size needs to be 0. What is going on? I am lost…

What I said is still true but I am making an exception for the hovered image. I am telling the browser that only one image will have a size that’s not equal to zero — so it will contribute to the dimension of the grid — while all the others remain equal to 0.

The left side shows the grid in its natural state without any hovered images, which is what the right side is showing. All the grid cells on the left side are equal in size since all the images have no physical dimensions.

On the right side, the second image in the first row is hovered, which gives it dimensions that affect the grid cell’s size. The browser will make that specific grid cell bigger on hover, which contributes to the overall size. And since the size of the whole grid is set (because we set a fixed width on the .gallery), the other grid cells will logically respond by becoming smaller in order to keep the .gallery‘s overall size in tact.

That’s our zoom effect in action! By increasing the size of only one image we affect the whole grid configuration, and we said before that the grid defines the size of the images so that each image stretches inside its grid cell to fill all the space.

To this, we add a touch of transition and use object-fit to avoid image distortion and the illusion is perfect!

I know that the logic behind the trick is not easy to grasp. Don’t worry if you don’t fully understand it. The most important is to understand the structure of the code used and how to modify it to get more variations. That’s what we will do next!

Adding more images

We created a 3×3 grid to explain the main trick, but you have probably guessed that we there’d no need to stop there. We can make the number of columns and rows variables and add as many images as we want.

.gallery {
  --n: 3; /* number of rows*/
  --m: 4; /* number of columns */
  --s: 150px; /* control the size */
  --g: 10px;  /* control the gap */
  --f: 1.5;   /* control the scale factor */

  display: grid;
  gap: var(--g);
  width:  calc(var(--m)*var(--s) + (var(--m) - 1)*var(--g));
  height: calc(var(--n)*var(--s) + (var(--n) - 1)*var(--g));
  grid-template-columns: repeat(var(--m),auto);
}

We have two new variables for the number of rows and columns. Then we simply define the width and height of our grid using them. Same for grid-template-columns which uses the --m variable. And just like before, we don’t need to explicitly define the rows since the CSS Grid’s auto-placement feature will do the job for us no matter how many image elements we’re using.

Why not different values for the width and height? We can do that:

.gallery {
  --n: 3; /* number of rows*/
  --m: 4; /* number of columns */
  --h: 120px; /* control the height */
  --w: 150px; /* control the width */
  --g: 10px;  /* control the gap */
  --f: 1.5;   /* control the scale factor */

  display: grid;
  gap: var(--g);
  width:  calc(var(--m)*var(--w) + (var(--m) - 1)*var(--g));
  height: calc(var(--n)*var(--h) + (var(--n) - 1)*var(--g));
  grid-template-columns: repeat(var(--m),auto);
}

.gallery img:hover{
  width:  calc(var(--w)*var(--f));
  height: calc(var(--h)*var(--f));
}

We replace --s with two variables, one for the width, --w, and another one for the height, --h. Then we adjust everything else accordingly.

So, we started with a grid with a fixed size and number of elements, but then we made a new set of variables to get any configuration we want. All we have to do is to add as many images as we want and adjust the CSS variables accordingly. The combinations are limitless!

What about a full-screen version? Yes, that’s also possible. All we need is to know what values we need to assign to our variables. If we want N rows of images and we want our grid to be full screen, we first need to solve for a height of 100vh:

var(--n) * var(--h) + (var(--n) - 1) * var(--g) = 100vh

Same logic for the width, but using vw instead of vh:

var(--m) * var(--w) + (var(--m) - 1) * var(--g) = 100vw

We do the math to get:

--w: (100vw - (var(--m) - 1) * var(--g)) / var(--m)
--h: (100vh - (var(--n) - 1) * var(--g)) / var(--n)

Done!

It’s the same exact HTML but with some updated variables that change the grid’s sizing and behavior.

Note that I have omitted the formula we previously set on the .gallery‘s width and height and replaced them with 100vw and 100vh, respectively. The formula will give us the same result but since we know what value we want, we can ditch all that added complexity.

We can also simplify the --h and --w by removing the gap from the equation in favor of this:

--h: calc(100vh / var(--n)); /* Viewport height divided by number of rows */
--w: calc(100vw / var(--m)); /* Viewport width divided by number of columns */

This will make the hovered image grow a bit more than the previous example, but it is no big deal since we can control the scale with the --f variable we’re using as a multiplier.

And since the variables are used in one place we can still simplify the code by removing them altogether:

It’s important to note this optimization applies only to the full-screen example and not to the examples we’ve covered. This example is a particular case where we can make the code lighter by removing some of the complex calculation work we needed in the other examples.

We actually have everything we need to create the popular pattern of expanding panels:

Let’s dig even deeper

Did you notice that our scale factor can be less than 1? We can define the size of the hovered image to be smaller than --h or --w but the image gets bigger on hover.

The initial grid cell size is equal to --w and --h, so why do a smaller values make the grid cell bigger? Shouldn’t the cell get smaller, or at least maintain its initial size? And what is the final size of the grid cell?

We need to dig deeper into how the CSS Grid algorithm calculates the size of the grid cells. And this is involves understanding CSS Grid’s default stretch alignment.

Here’s an example to understand the logic.

On the left side of the demo, I defined a two-column with auto width. We get the intuitive result: two equal columns (and two equal grid cells). But the grid I set up on the right side of the demo, where I am updating the alignment using place-content: start, appears to have nothing.

DevTools helps show us what’s really happening in both cases:

In the second grid, we have two columns, but their widths equal zero, so we get two grid cells that are collapsed at the top-left corner of the grid container. This is not a bug but the logical result of the grid’s alignment. When we size a column (or row) with auto, it means that its content dictates its size — but we have an empty div with no content to make room for.

But since stretch is the default alignment and we have enough space inside our grid, the browser will stretch both grid cells equally to cover all that area. That’s how the grid on the left winds up with two equal columns.

From the specification:

Note that certain values of justify-content and align-content can cause the tracks to be spaced apart (space-around, space-between, space-evenly) or to be resized (stretch).

Note the “to be resized” which is the key here. In the last example, I used place-content which is the shorthand for justify-content and align-content

And this is buried somewhere in the Grid Sizing algorithm specs:

This step expands tracks that have an auto max track sizing function by dividing any remaining positive, definite free space equally amongst them. If the free space is indefinite, but the grid container has a definite min-width/height, use that size to calculate the free space for this step instead.

“Equally” explains why we wind up with equal grid cells, but it applies to “the free space” which is very important.

Let’s take the previous example and add content to one of the divs:

We added a square 50px image. Here’s an illustration of how each grid in our example responds to that image:

In the first case, we can see that the first cell (in red) is bigger than the second one (in blue). In the second case, the size of the first cell changes to fit the physical size of the image while the second cell remains with no dimensions. The free space is divided equally, but the first cell has more content inside which makes it bigger.

This is the math to figure out our free space:

(grid width) - (gap) - (image width) = (free space)
200px - 5px - 50px = 145px 

Divided by two — the number of columns — we get a width of 72.5px for each column. But we add the size of the image, 50px, to the first column which leaves us with one column at 122.5px and the second one equal to 72.5px.

The same logic applies to our grid of images. All the images have a size equal to 0 (no content) while the hovered image contributes to size — even if it’s just 1px — making its grid cell bigger than the others. For this reason, the scale factor can be any value bigger than 0 even decimals between 0 and 1.

To get the final width of the grid cells, we do the same calculation to get the following:

(container width) - (sum of all gaps) - (hovered image width) = (free space)

The width of container is defined by:

var(--m)*var(--w) + (var(--m) - 1)*var(--g)

…and all the gaps are equal to:

(var(--m) - 1)*var(--g)

…and for the hovered image we have:

var(--w)*var(--f)

We can calculate all of that with our variables:

var(--m)*var(--w) - var(--w)*var(--f) = var(--w)*(var(--m) - var(--f))

The number of columns is defined by --m ,so we divide that free space equally to get:

var(--w)*(var(--m) - var(--f))/var(--m)

…which gives us the size of the non-hovered images. For hovered images, we have this:

var(--w)*(var(--m) - var(--f))/var(--m) + var(--w)*var(--f)
var(--w)*((var(--m) - var(--f))/var(--m) + var(--f))

If we want to control the final size of the hovered image, we consider the above formula to get the exact size we want. If, for example, we want the image to be twice as big:

(var(--m) - var(--f))/var(--m) + var(--f) = 2

So, the value of our scale multiplier, --f, needs to be equal to:

var(--m)/(var(--m) - 1)

For three columns we will have 3/2 = 1.5 and that’s the scale factor I used in the first demo of this article because I wanted to make the image twice as big on hover!

The same logic applies to the height calculation and in case we want to control both of them independently we will need to consider two scale factors to make sure we have a specific width and height on hover.

.gallery {
  /* same as before */
   --fw: 1.5; /* controls the scale factor for the width */
   --fh: 1.2; /* controls the scale factor for the height */

  /* same as before */
}

.gallery img:hover{
  width:  calc(var(--w)*var(--fw));
  height: calc(var(--h)*var(--fh));
}

Now, you know all the secrets to create any kind of image grid with a cool hover effect while also having control of the sizing you want using the math we just covered.

Wrapping up

In my last article, we created a complex-looking grid with a few lines of CSS that put CSS Grid’s implicit grid and auto-placement features to use. In this article, we relied on some CSS Grid sizing trickery to create a fancy grid of images that zoom on hover and cause the grid to adjust accordingly. All of this with a simplified code that is easy to adjust using CSS variables!

In the next article, we will play with shapes! We will combine CSS grid with mask and clip-path to get fancy grid of images.


Zooming Images in a Grid Layout originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/zooming-images-in-a-grid-layout/feed/ 3 367204
Implicit Grids, Repeatable Layout Patterns, and Danglers https://css-tricks.com/implicit-grids-repeatable-layout-patterns-and-danglers/ https://css-tricks.com/implicit-grids-repeatable-layout-patterns-and-danglers/#comments Tue, 02 Aug 2022 13:10:49 +0000 https://css-tricks.com/?p=367158 Dave Rupert with some modern CSS magic that tackles one of those classic conundrums: what happens when the CSS for component is unable to handle the content we throw at it?

The specific situation is when a layout grid expects …


Implicit Grids, Repeatable Layout Patterns, and Danglers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Dave Rupert with some modern CSS magic that tackles one of those classic conundrums: what happens when the CSS for component is unable to handle the content we throw at it?

The specific situation is when a layout grid expects an even number of items, but is supplied with an odd number instead. We’re left with a “dangling” element at the end that throws off the layout. Sounds like what’s needed is some Defensive CSS and Dave accomplishes it.

He reaches for :has() to write a nifty selector that sniffs out the last item in a grid that contains an odd number of items:

.items:has(.item:last-of-type:nth-of-type(odd)) .item:first-of-type { }

Breaking that down:

  • We have a parent container of .items.
  • If the container :has() an .item child that is the last of its type,
  • …and that .item happens to be an odd-numbered instance,
  • …then select the first .item element of that type and style it!

In this case, that last .item can be set to go full-width to prevent holes in the layout.

If… then… CSS has conditional logic powers! We’re only talking about support for Safari TP and Edge/Chrome Canary at the moment, but that’s pretty awesome.

As chance has it, Temani Afif recently shared tricks he learned while experimenting with implicit grids. By taking advantage of CSS Grid’s auto-placement algorithm, we don’t even have to explicitly declare a fixed number of columns and rows for a grid — CSS will create them for us if they’re needed!

No, Temani’s techniques aren’t alternative solutions to Dave’s “dangler” dilemma. But combining Temani’s approach to repeatable grid layout patterns with Dave’s defensive CSS use of :has(), we get a pretty powerful and complex-looking grid that’s lightweight and capable of handling any number of items while maintaining a balanced, repeatable pattern.


Implicit Grids, Repeatable Layout Patterns, and Danglers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/implicit-grids-repeatable-layout-patterns-and-danglers/feed/ 2 367158
Exploring CSS Grid’s Implicit Grid and Auto-Placement Powers https://css-tricks.com/exploring-css-grids-implicit-grid-and-auto-placement-powers/ https://css-tricks.com/exploring-css-grids-implicit-grid-and-auto-placement-powers/#comments Mon, 01 Aug 2022 13:44:13 +0000 https://css-tricks.com/?p=367178 When working with CSS Grid, the first thing to do is to set display: grid on the element that we want to be become a grid container. Then we explicitly define the grid using a combination of grid-template-columns, grid-template-rows


Exploring CSS Grid’s Implicit Grid and Auto-Placement Powers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
When working with CSS Grid, the first thing to do is to set display: grid on the element that we want to be become a grid container. Then we explicitly define the grid using a combination of grid-template-columns, grid-template-rows, and grid-template-areas. And from there, the next step is to place items inside the grid.

This is the classic approach that should be used and I also recommend it. However, there is another approach for creating grids without any explicit definition. We call this the implicit grid.

“Explicit, implicit? What the heck is going on here?”

Strange terms, right? Manuel Matuzovic already has a good explanation of what we may by “implicit” and “explicit” in CSS Grid, but let’s dig straight into what the specification says:

The grid-template-rows, grid-template-columns, and grid-template-areas properties define a fixed number of tracks that form the explicit grid. When grid items are positioned outside of these bounds, the grid container generates implicit grid tracks by adding implicit grid lines to the grid. These lines together with the explicit grid form the implicit grid.

So, in plain English, the browser auto-generates extra rows and columns in case any elements happen to be placed outside the defined grid.

What about auto-placement?

Similar to the concept of implicit grid, auto-placement is the ability of the browser to automatically place the items inside the grid. We don’t always need to give the position of each item.

Through different use cases, we are going to see how such features can help us create complex and dynamic grid with a few lines of code.

Dynamic sidebar

Here, we have three different layouts but we only have one grid configuration that works for all of them.

main {
  display: grid;
  grid-template-columns: 1fr;
}

Only one column is taking up all the free space. This is our “explicit” grid. It’s set up to fit one grid item in the main grid container. That’s all. One column and one row:

But what if we decided to drop another element in there, say an aside (our dynamic sidebar). As it’s currently (and explicitly) defined, our grid will have to adjust automatically to find a place for that element. And if we do nothing else with our CSS, here’s what DevTools tells us is happening.

The element takes up the entire column that is explicitly set on the container. Meanwhile, the falls onto a new row between implicit grid lines labeled 2 and 3. Note that I’m using a 20px gap to help separate things visually.

We can move the <aside> to a column beside the <section>:

aside {
  grid-column-start: 2;
}

And here’s what DevTools tells us now:

The element is between the grid container’s first and second grid column lines. The starts at the second grid column line and ends at a third line we never declared.

We place our element in the second column but… we don’t have a second column. Weird, right? We never declared a second column on the <main> grid container, but the browser created one for us! This is the key part from the specification we looked at:

When grid items are positioned outside of these bounds, the grid container generates implicit grid tracks by adding implicit grid lines to the grid.

This powerful feature allows us to have dynamic layouts. If we only have the <section> element, all we get is one column. But if we add an <aside> element to the mix, an extra column is created to contain it.

We could place the <aside> before the <section> instead like this:

aside {
  grid-column-end: -2;
} 

This creates the implicit column at the start of the grid, unlike the previous code that places the implicit column at the end.

We can have either a right or left sidebar

We can do the same thing more easily using the grid-auto-flow property to set any and all implicit tracks to flow in a column direction:

Now there’s no need to specify grid-column-start to place the <aside> element to the right of the <section>! In fact, any other grid item we decide to throw in there at any time will now flow in a column direction, each one placed in its own implicit grid tracks. Perfect for situations where the number of items in the grid isn’t known in advance!

That said, we do still need grid-column-end if we want to place it in a column to the left of it because, otherwise, the <aside> will occupy the explicit column which, in turn, pushes the <section> outside the explicit grid and forces it to take the implicit column.

I know, I know. That’s a little convoluted. Here is another example we can use to better understand this little quirk:

In the first example, we didn’t specify any placement. In this case, the browser will first place the <aside> element in the explicit column since it comes first in the DOM. The <section>, meanwhile, is automatically placed in the grid column the browser automatically (or implicitly) creates for us.

In the second example, we set the <aside> element outside of the explicit grid:

aside {
  grid-column-end: -2;
}

Now it doesn’t matter that <aside> comes first in the HTML. By reassigning <aside> somewhere else, we’ve made the <section> element available to take the explicit column.

Image grid

Let’s try something different with a grid of images where we have a big image and a few thumbnails beside it (or under it).

We have two grid configurations. But guess what? I am not defining any grid at all! All I am doing is this:

.grid img:first-child {
  grid-area: span 3 / span 3;
}

It’s surprising we only need one line of code to pull off something like this, so let’s dissect what’s going on and you will see that it’s easier than you may think. First of all, grid-area is a shorthand property that combines the following properties into a single declaration:

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

Wait! Isn’t grid-area the property we use to define named areas instead of where elements start and end on the grid?

Yes, but it also does more. We could write a whole lot more about grid-area, but in this particular case:

.grid img:first-child {
  grid-area: span 3 / span 3;
}

/* ...is equivalent to: */
.grid img:first-child {
  grid-row-start: span 3;
  grid-column-start: span 3;
  grid-row-end: auto;
  grid-column-end: auto;
}

We can see the same thing when cracking open DevTools to expand the shorthand version:

This means that the first image element in the grid needs to span three columns and three rows. But since we didn’t define any columns or rows, the browser does it for us.

We’ve essentially placed the first image in the HTML to take up a 3⨉3 grid. That means that any other images will be placed automatically in those same three columns without the need to specify anything new.

To summarize, we told the browser that the first image needs take up the space of three columns and three rows that we never explicitly defined when setting up the grid container. The browser set those columns and rows up for us. As a result, the remaining images in the HTML flow right into place using the same three columns and rows. And since the first image takes up all three columns in the first row, the remaining images flow into additional rows that each contain three columns, where each image takes up a single column.

All this from one line of CSS! That’s the power of “implicit” grid” and auto-placement.

For the second grid configuration in that demo, all I’ve done is change the automatic flow direction using grid-auto-flow: column the same way we did earlier when placing an <aside> element next to a <section>. This forces the browser to create a fourth column it can use to place the remaining images. And since we have three rows, the remaining images get placed inside the same vertical column.

We need to add a few properties to the images to make sure they fit nicely inside the grid without any overflow:

.grid {
  display: grid;
  grid-gap: 10px;
}

/* for the second grid configuration */
.horizontal {
  grid-auto-flow: column;
}

/* The large 3⨉3 image */
.grid img:first-child {
  grid-area: span 3 / span 3;
}

/* Help prevent stretched or distorted images */
img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

And of course, we can easily update the grid to consider more images by adjusting one value. That would be the 3 in the styles for the large image. We have this:

.grid img:first-child {
  grid-area: span 3 / span 3;
}

But we could add a fourth column simply by changing it to 4 instead:

.grid img:first-child {
  grid-area: span 4 / span 4;
}

Even better: let’s set that up as a custom property to make things even easier to update.

Dynamic layouts

The first use case with the sidebar was our first dynamic layout. Now we will tackle more complex layouts where the number of elements will dictate the grid configuration.

In this example, we can have anywhere from one to four elements where the grid adjusts in way that nicely fits the number of elements without leaving any awkward gaps or missing spaces.

When we have one element, we do nothing. The element will stretch to fill the only row and column automatically created by the grid.

Bit when we add the second element, we create another (implicit) column using grid-column-start: 2.

When we add a third element, it should take up the width of two columns — that’s why we used grid-column-start: span 2, but only if it’s the :last-child because if (and when) we add a fourth element, that one should only take up a single column.

Adding that up, we have four grid configurations with only two declarations and the magic of implicit grid:

.grid {
  display: grid;
}
.grid :nth-child(2) {
  grid-column-start: 2;
}
.grid :nth-child(3):last-child {
  grid-column-start: span 2;
}

Let’s try another one:

We’re doing nothing for the first and second cases where we have only one or two elements. When we add a third element, though, we tell the browser that — as long as it’s the :last-child — it should span two columns. When we add a fourth element, we tell the browser that element needs to be placed in the second column.

.grid {
  display: grid;
}
.grid :nth-child(3):last-child {
  grid-column-start: span 2;
}
.grid :nth-child(4) {
  grid-column-start: 2;
}

Are you starting to get the trick? We give the browser specific instructions based on the number of elements (using :nth-child) and, sometimes, one instruction can change the layout completely.

It should be noted that the sizing will not be the same when we work with different content:

Since we didn’t define any sizes for our items, the browser automatically sizes them for us based on their contents and we may end up with different sizing than what we just saw. To overcome this, we have to explicitly specify that all the columns and rows are equally sized:

grid-auto-rows: 1fr;
grid-auto-columns: 1fr;

Hey, we haven’t played with those properties yet! grid-auto-rows and grid-auto-columns set the size of implicit rows and columns, respectively, in a grid container. Or, as the spec explains it:

The grid-auto-columns and grid-auto-rows properties specify the size of tracks not assigned a size by grid-template-rows or grid-template-columns.

Here is another example where we can go up to six elements. This time I will let you dissect the code. Don’t worry, the selectors may look complex but the logic is pretty straightforward.

Even with six elements, we only needed two declarations. Imagine all the complex and dynamic layouts we can achieve with a few lines of code!

What’s going on with that grid-auto-rows and why does it take three values? Are we defining three rows?

No, we are not defining three rows. But we are defining three values as a pattern for our implicit rows. The logic is as follows:

  • If we have one row, it will get sized with the first value.
  • If we have two rows, the first one gets the first value and the second one the second value.
  • If we have three rows, the three values will get used.
  • If we have four rows (and here comes the interesting part), we use the three values for the first three rows and we reuse the first value again for the fourth row. That’s why it’s a kind of pattern that we repeat to size all the implicit rows.
  • If we have 100 rows, they will be sized three-by-three to have 2fr 2fr 1fr 2fr 2fr 1fr 2fr 2fr 1fr, etc.

Unlike grid-template-rows which defines the number of rows and their sizes, grid-auto-rows only sizes row that may get created along the way.

If we get back to our example, the logic is to have equal size when two rows are created (we will use the 2fr 2fr), but if a third row is created we make it a bit smaller.

Grid patterns

For this last one, we are going to talk about patterns. You have probably seen those two column layouts where one column is wider than the other, and each row alternates the placement of those columns.

This sort layout can be difficult too pull off without knowing exactly how much content we’re dealing with, but CSS Grid’s auto-placement powers makes it a relative cinch.

Take a peek at the code. It may look complex but let’s break it down because it winds up being pretty straightforward.

The first thing to do is to identify the pattern. Ask yourself: “After how many elements should the pattern repeat?” In this case it’s after every four elements. So, let’s look at using only four elements for now:

Now, let’s define the grid and set up the general pattern using the :nth-child selector for alternating between elements:

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows equal to 100px */
}
.grid :nth-child(4n + 1) { /* ?? */ }
.grid :nth-child(4n + 2) { /* ?? */ }
.grid :nth-child(4n + 3) { /* ?? */ }
.grid :nth-child(4n + 4) { /* ?? */ }

We said that our pattern repeats every four elements, so we will logically use 4n + x where x ranges from 1 to 4. It’s a little easier to explain the pattern this way:

4(0) + 1 = 1 = 1st element /* we start with n = 0 */
4(0) + 2 = 2 = 2nd element
4(0) + 3 = 3 = 3rd element
4(0) + 4 = 4 = 4th element
4(1) + 1 = 5 = 5th element /* our pattern repeat here at n = 1 */
4(1) + 2 = 6 = 6th element
4(1) + 3 = 7 = 7th element
4(1) + 4 = 8 = 8th element
4(2) + 1 = 9 = 9th element /* our pattern repeat again here at n = 2 */
etc.

Perfect, right? We have four elements, and repeat the pattern on the fifth element, the ninth element and so on.

Those :nth-child selectors can be tricky! Chris has a super helpful explanation of how it all works, including recipes for creating different patterns.

Now we configure each element so that:

  1. The first element needs to take two columns and start at column one (grid-column: 1/span 2).
  2. The second element is placed in the third column (grid-column-start: 3).
  3. The third element is placed at the first column: (grid-column-start: 1).
  4. The fourth element takes two columns and starts at the second column: (grid-column: 2/span 2).

Here that is in CSS:

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1) { grid-column: 1/span 2; }
.grid :nth-child(4n + 2) { grid-column-start: 3; }
.grid :nth-child(4n + 3) { grid-column-start: 1; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

We could stop here and be done… but we can do better! Specifically, we can remove some declarations and rely grid’s auto-placement powers to do the job for us. This is the trickiest part to grok and requires a lot of practice to be able to identify what can be removed.

The first thing we can do is update grid-column: 1 /span 2 and use only grid-column: span 2 since, by default, the browser will place the first item into the first column. We can also remove this:

.grid :nth-child(4n + 3) { grid-column-start: 1; }

By placing the first, second, and fourth items, the grid automatically places the third item in the correct place. That means we’re left with this:

.grid {
  display: grid;
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
  grid-auto-columns: 1fr; /* all the columns are equal */
}
.grid :nth-child(4n + 1) { grid-column: span 2; }
.grid :nth-child(4n + 2) { grid-column-start: 3; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

But c’mon we can stroll do better! We can also remove this:

.grid :nth-child(4n + 2) { grid-column-start: 3; }

Why? If we place the fourth element in the second column while allowing it to take up two full columns, we’re forcing the grid to create a third implicit column, giving us a total of three columns without explicitly telling it to. The fourth element cannot go into the first row since the first item is also taking two columns, so it flows to the next row. This configuration leave us with an empty column in the first row and an empty one in the second row.

I think you know the end of the story. The browser will automatically place the second and third items in those empty spots. So our code becomes even simpler:

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1) { grid-column: span 2; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

All it takes is five declarations to create a very cool and very flexible pattern. The optimization part may be tricky, but you get used to it and gain some tricks with practice.

Why not use grid-template-columns to define explicit columns since we know the number of columns?

We can do that! Here’s the code for it:

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1),
.grid :nth-child(4n + 4) {
  grid-column: span 2;
}

As you can see, the code is definitely more intuitive. We define three explicit grid columns and we tell the browser that the first and fourth elements need to take two columns. I highly recommend this approach! But the goal of this article is to explore new ideas and tricks that we get from CSS Grid’s implicit and auto-placement powers.

The explicit approach is more straightforward, while an implicit grid requires you to — pardon the pun — fill in the gaps where CSS is doing additional work behind the scenes. In the end, I believe that having a solid understanding of implicit grids will help you better understand the CSS Grid algorithm. After all, we are not here to study what’s obvious — we are here to explore wild territories!

Let’s try another pattern, a bit quicker this time:

Our pattern repeats every six elements. The third and fourth elements each need to occupy two full rows. If we place the third and the fourth elements, it seems that we don’t need to touch the others, so let’s try the following:

.grid {
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-rows: 100px;
}
.grid :nth-child(6n + 3) {
  grid-area: span 2/2; /* grid-row-start: span 2 && grid-column-start: 2 */
}
.grid :nth-child(6n + 4) {
  grid-area: span 2/1; /* grid-row-start: span 2 && grid-column-start: 1 */
}

Hmm, no good. We need to place the second element in the first column. Otherwise, the grid will automatically place it in the second column.

.grid :nth-child(6n + 2) {
  grid-column: 1; /* grid-column-start: 1 */
}

Better, but there’s still more work, We need to shift the third element to the top. It’s tempting to try placing it in the first row this way:

.grid :nth-child(6n + 3) {
  grid-area: 1/2/span 2; 
    /* Equivalent to:
       grid-row-start: 1;
       grid-row-end: span 2;
       grid-column-start: 2 
     */
}

But this doesn’t work because it forces all the 6n + 3 elements to get placed in the same area which makes a jumbled layout. The real solution is to keep the initial definition of the third element and add grid-auto-flow: dense to fill the gaps. From MDN:

[The] “dense” packing algorithm attempts to fill in holes earlier in the grid, if smaller items come up later. This may cause items to appear out-of-order, when doing so would fill in holes left by larger items. If it is omitted, a “sparse” algorithm is used, where the placement algorithm only ever moves “forward” in the grid when placing items, never backtracking to fill holes. This ensures that all of the auto-placed items appear “in order”, even if this leaves holes that could have been filled by later items.

I know this property is not very intuitive but never forget it when you face a placement issue. Before trying different configurations in vain, add it because it may fix your layout with no additional effort.

Why not always add this property by default?

I don’t recommend it because, in some cases, we don’t want that behavior. Note how the MDN’s explanation there mentions it causes items to flow “out-of-order” to fill holes left by larger items. Visual order is usually just as important as the source order, particularly when it comes to accessible interfaces, and grid-auto-flow: dense can sometimes cause a mismatch between the visual and source order.

Our final code is then:

.grid {
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-flow: dense;
  grid-auto-rows: 100px;
}
.grid :nth-child(6n + 2) { grid-column: 1; }
.grid :nth-child(6n + 3) { grid-area: span 2/2; }
.grid :nth-child(6n + 4) { grid-row: span 2; }

Another one? Let’s go!

For this one, I will not talk too much and instead show you an illustration of the code I have used. Try to see if you get how I reached that code:

The items in black are implicitly placed in the grid. It should be noted that we can get the same layout more ways than how I got there. Can you figure those out, too? What about using grid-template-columns? Share your works in the comment section.

I am gonna leave you with a last pattern:

I do have a solution for this one but it’s your turn to practice. Take all that we have learned and try to code this by yourself and then compare it with my solution. Don’t worry if you end with something verbose — the most important thing is finding a working solution.

Want more?

Before we end I want to share a few Stack Overflow questions related to CSS Grid where I jumped in with answers that use many of the techniques we covered here together. It’s a good list that shows just how many real use cases and real-world situations come up where these things come in handy:

Wrapping up

CSS Grid has been around for years, but there are still a lot of little-known and used tricks that aren’t widely discussed. The implicit grid and auto-placement features are two of them!

And yes, this can get challenging! It has taken me a lot of time to grok the logic behind implicit grids and I still struggle with auto-placement. If you want to spend more time wrapping your head around explicit and implicit grids, here are a couple of additional explanations and examples worth checking out:

Similarly, you might want to read about grid-auto-columns in the CSS-Tricks Almanac because Mojtaba Seyedi goes into great detail and includes incredibly helpful visuals to help explain the behavior.

Like I said when we started, the methods we covered here are not meant to replace the common ways you already know for building grids. I am simply exploring different ways that can be helpful in some cases.


Exploring CSS Grid’s Implicit Grid and Auto-Placement Powers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/exploring-css-grids-implicit-grid-and-auto-placement-powers/feed/ 14 367178
In Praise of Shadows https://css-tricks.com/in-praise-of-shadows/ https://css-tricks.com/in-praise-of-shadows/#respond Tue, 12 Jul 2022 17:08:49 +0000 https://css-tricks.com/?p=366896 Our dear friend Robin has a new essay called In Praise of Shadows. Now, before you hop over there looking for nuggets on CSS box shadows, text shadows, and shadow filters… this is not that. It’s an essay …


In Praise of Shadows originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Our dear friend Robin has a new essay called In Praise of Shadows. Now, before you hop over there looking for nuggets on CSS box shadows, text shadows, and shadow filters… this is not that. It’s an essay on photography and what Robin has learned about handing shadows with a camera.

So, why share this? Because it’s cool as heck that he made an article directed page dedicated to one essay. And you’ll learn a lot about CSS if you crack open DevTools on it:

  • Centering techniques. Notice how CSS Grid is used on the <body> simply to center the pamphlet. Then Robin reaches for it again on each .frame of the essay to do the same thing with the content.
  • “Faux” background images. Robin could have made a lot of work for himself by creating a CSS class for each .frame to get the background images. Instead, he uses object-fit: cover on inlined HTML <img>s to maintain the aspect ratio while filling the .frame container. (He’s actually written about this before.) That sure saves a lot of CSS’ing, but it also allows him to use alt text if needed. I sorta wonder if a <figure>/<figcaption> structure could’ve worked here instead but I doubt it would provide much additional benefit for what’s going on.
  • Stacking contexts. Another perk of those faux background images? They use absolute positioning which creates a stacking context, allowing Robin to set a z-index: 0 on the images. That way, the text stacks directly on top with z-index: 1. Again, CSS Grid is handling all the centering so things are nicely aligned.
  • Scroll snapping. I always love it when I see CSS scroll snapping in the wild. Robin made a nice choice to use it here, as it really lends to the whole page-turning experience while scrolling through frames. Horizontal scrolling can be maddening, but also extremely elegant when executed well as it is here in how it enhances the narrow-column design. If you want a nice explanation of how it all works, Robin has also written about horizontal scroll snapping.

If nothing else, Robin is an excellent writer and it’s worth reading anything he publishes, CSS and beyond.

To Shared LinkPermalink on CSS-Tricks


In Praise of Shadows originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/in-praise-of-shadows/feed/ 0 366896
Conditionally Styling Selected Elements in a Grid Container https://css-tricks.com/conditionally-styling-selected-elements-in-a-grid-container/ https://css-tricks.com/conditionally-styling-selected-elements-in-a-grid-container/#comments Wed, 15 Jun 2022 14:15:50 +0000 https://css-tricks.com/?p=366252 Calendars, shopping carts, galleries, file explorers, and online libraries are some situations where selectable items are shown in grids (i.e. square lattices). You know, even those security checks that ask you to select all images with crosswalks or whatever.

🧐…


Conditionally Styling Selected Elements in a Grid Container originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Calendars, shopping carts, galleries, file explorers, and online libraries are some situations where selectable items are shown in grids (i.e. square lattices). You know, even those security checks that ask you to select all images with crosswalks or whatever.

🧐

I found a neat way to display selectable options in a grid. No, not recreating that reCAPTCHA, but simply being able to select multiple items. And when two or more adjoining items are selected, we can use clever :nth-of-type combinators, pseudo elements, and the :checked pseudo-class to style them in a way where they look grouped together.

The whole idea of combinators and pseudos to get the rounded checkboxes came from a previous article I wrote. It was a simple single-column design:

This time, however, the rounding effect is applied to elements along both the vertical and horizontal axes on a grid. You don’t have to have read my last article on checkbox styling for this since I’m going to cover everything you need to know here. But if you’re interested in a slimmed down take on what we’re doing in this article, then that one is worth checking out.

Before we start…

It’ll be useful for you to take note of a few things. For example, I’m using static HTML and CSS in my demo for the sake of simplicity. Depending on your application you might have to generate the grid and the items in it dynamically. I’m leaving out practical checks for accessibility in order to focus on the effect, but you would definitely want to consider that sort of thing in a production environment.

Also, I’m using CSS Grid for the layout. I’d recommend the same but, of course, it’s only a personal preference and your mileage may vary. For me, using grid allows me to easily use sibling-selectors to target an item’s ::before and ::after pseudos.

Hence, whatever layout standard you might want to use in your application, make sure the pseudos can still be targeted in CSS and ensure the layout stays in tact across different browsers and screens.

Let’s get started now

As you may have noticed in the earlier demo, checking and unchecking a checkbox element modifies the design of the boxes, depending on the selection state of the other checkboxes around it. This is possible because I styled each box using the pseudo-elements of its adjacent elements instead of its own element.

The following figure shows how the ::before pseudo-elements of boxes in each column (except the first column) overlap the boxes to their left, and how the ::after pseudo-elements of boxes in each row (except the first row) overlap the boxes above.

Two grids of checkboxes showing the placement of before and after pseudos.

Here’s the base code

The markup is pretty straightforward:

<main>
  <input type=checkbox> 
  <input type=checkbox> 
  <input type=checkbox>
  <!-- more boxes -->
</main>

There’s a little more going on in the initial CSS. But, first, the grid itself:

/* The grid */
main {
  display: grid;
  grid:  repeat(5, 60px) / repeat(4, 85px);
  align-items: center;
  justify-items: center;
  margin: 0;
}

That’s a grid of five rows and four columns that contain checkboxes. I decided to wipe out the default appearance of the checkboxes, then give them my own light gray background and super rounded borders:

/* all checkboxes */
input {
  -webkit-appearance: none;
  appearance: none;
  background: #ddd;
  border-radius: 20px;
  cursor: pointer;
  display: grid;
  height: 40px;
  width: 60px;
  margin: 0;
}

Notice, too, that the checkboxes themselves are grids. That’s key for placing their ::before and ::after pseudo-elements. Speaking of which, let’s do that now:

/* pseudo-elements except for the first column and first row */
input:not(:nth-of-type(4n+1))::before,
input:nth-of-type(n+5)::after {
  content: '';        
  border-radius: 20px;
  grid-area: 1 / 1;
  pointer-events: none;
}

We’re only selecting the pseudo-elements of checkboxes that are not in the first column or the first row of the grid. input:not(:nth-of-type(4n+1)) starts at the first checkbox, then selects the ::before of every fourth item from there. But notice we’re saying :not(), so really what we’re doing is skipping the ::before pseudo-element of every fourth checkbox, starting at the first. Then we’re applying styles to the ::after pseudo of every checkbox from the fifth one.

Now we can style both the ::before and ::after pseudos for each checkbox that is not in the first column or row of the grid, so that they are moved left or up, respectively, hiding them by default.

/* pseudo-elements other than the first column */
input:not(:nth-of-type(4n+1))::before { 
  transform: translatex(-85px);
}

/* pseudo-elements other than the first row */
input:nth-of-type(n+5)::after {
 transform: translatey(-60px); 
}

Styling the :checked state

Now comes styling the checkboxes when they are in a :checked state. First, let’s give them a color, say a limegreen background:

input:checked { background: limegreen; }

A checked box should be able to re-style all of its adjacent checked boxes. In other words, if we select the eleventh checkbox in the grid, we should also be able to style the boxes surrounding it at the top, bottom, left, and right.

A four-by-five grid of squares numbered one through 20. 11 is selected and 7, 10, 12, and 15 are highlighted.

This is done by targeting the correct pseudo-elements. How do we do that? Well, it depends on the actual number of columns in the grid. Here’s the CSS if two adjacent boxes are checked in a 5⨉4 grid:

/* a checked box's right borders (if the element to its right is checked) */
input:not(:nth-of-type(4n)):checked + input:checked::before { 
  border-top-right-radius: 0; 
  border-bottom-right-radius: 0; 
  background: limegreen;
}
/* a checked box's bottom borders (if the element below is checked) */
input:nth-last-of-type(n+5):checked + * + * + * + input:checked::after {
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
  background: limegreen;
}
/* a checked box's adjacent (right side) checked box's left borders */
input:not(:nth-of-type(4n)):checked + input:checked + input::before {         
  border-top-left-radius: 0; 
  border-bottom-left-radius: 0; 
  background: limegreen;
}
/* a checked box's adjacent (below) checked box's top borders */
input:not(:nth-of-type(4n)):checked + * + * + * +  input:checked + input::before { 
  border-top-left-radius: 0; 
  border-top-right-radius: 0; 
  background: limegreen;
}

If you prefer you can generate the above code dynamically. However, a typical grid, say an image gallery, the number of columns will be small and likely a fixed number of items, whereas the rows might keep increasing. Especially if designed for mobile screens. That’s why this approach is still an efficient way to go. If for some reason your application happens to have limited rows and expanding columns, then consider rotating the grid sideways because, with a stream of items, CSS Grid arranges them left-to-right and top-to-bottom (i.e. row by row).

We also need to add styling for the last checkboxes in the grid — they’re not all covered by pseudo-elements as they are the last items in each axis.

/* a checked box's (in last column) left borders */
input:nth-of-type(4n-1):checked + input:checked {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
/* a checked box's (in last column) adjacent (below) checked box's top borders */
input:nth-of-type(4n):checked + * + * + * + input:checked {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

Those are some tricky selectors! The first one…

input:nth-of-type(4n-1):checked + input:checked

…is basically saying this:

A checked <input> element next to a checked <input> in the second last column.

And the nth-of-type is calculated like this:

4(0) - 1 = no match
4(1) - 1 = 3rd item
4(2) - 1 = 7th item
4(3) - 1 = 11th item
etc.

So, we’re starting at the third checkbox and selecting every fourth one from there. And if a checkbox in that sequence is checked, then we style the checkboxes adjacent, too, if they are also checked.

And this line:

input:nth-of-type(4n):checked + * + * + * + input:checked

Is saying this:

An <input> element provided that is checked, is directly adjacent to an element, which is directly adjacent to another element, which is also directly adjacent to another element, which, in turn, is directly adjacent to an <input> element that is in a checked state.

What that means is we’re selecting every fourth checkbox that is checked. And if a checkbox in that sequence is checked, then we style the next fourth checkbox from that checkbox if it, too, is checked.

Putting it to use

What we just looked at is the general principle and logic behind the design. Again, how useful it is in your application will depend on the grid design.

I used rounded borders, but you can try other shapes or even experiment with background effects (Temani has you covered for ideas). Now that you know how the formula works, the rest is totally up to your imagination.

Here’s an instance of how it might look in a simple calendar:

Again, this is merely a rough prototype using static markup. And, there would be lots and lots of accessibility considerations to consider in a calendar feature.


That’s a wrap! Pretty neat, right? I mean, there’s nothing exactly “new” about what’s happening. But it’s a good example of selecting things in CSS. If we have a handle on more advanced selecting techniques that use combinators and pseudos, then our styling powers can reach far beyond the styling one item — as we saw, we can conditionally style items based on the state of another element.


Conditionally Styling Selected Elements in a Grid Container originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/conditionally-styling-selected-elements-in-a-grid-container/feed/ 3 366252
An Auto-Filling CSS Grid With Max Columns of a Minimum Size https://css-tricks.com/an-auto-filling-css-grid-with-max-columns/ https://css-tricks.com/an-auto-filling-css-grid-with-max-columns/#comments Wed, 16 Feb 2022 15:06:57 +0000 https://css-tricks.com/?p=363541 Within Drupal 10 core, we’re implementing a new auto-filling CSS Grid technique that I think is cool enough to share with the world.

The requirements are:

  • The user specifies a maximum number of columns. This is the auto-filling grid’s “natural”


An Auto-Filling CSS Grid With Max Columns of a Minimum Size originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Within Drupal 10 core, we’re implementing a new auto-filling CSS Grid technique that I think is cool enough to share with the world.

The requirements are:

  • The user specifies a maximum number of columns. This is the auto-filling grid’s “natural” state.
  • If a grid cell goes under a user-specified width, the auto-filling grid will readjust itself and decrease the number of columns.
  • The grid cells should always stretch to fit the auto-filling grid container’s width, no matter the column count.
  • All of this should work independent of viewport width and should not require JavaScript.
Screenshot showing the auto-filling CSS Grid's settings in Drupal.

The auto-filling CSS Grid in action

Here’s how the resulting auto-filling CSS grid behaves when it is compressed by the draggable div element to its left.

Here’s the code

If you’re not looking for the theory behind the auto-filling grid, and just want to copy/paste code, here you go!

.grid-container {
  /**
   * User input values.
   */
  --grid-layout-gap: 10px;
  --grid-column-count: 4;
  --grid-item--min-width: 100px;

  /**
   * Calculated values.
   */
  --gap-count: calc(var(--grid-column-count) - 1);
  --total-gap-width: calc(var(--gap-count) * var(--grid-layout-gap));
  --grid-item--max-width: calc((100% - var(--total-gap-width)) / var(--grid-column-count));

  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(max(var(--grid-item--min-width), var(--grid-item--max-width)), 1fr));
  grid-gap: var(--grid-layout-gap);
}

Theory and tools behind the auto-filling CSS Grid

The code above uses several modern CSS tools including CSS Grid’s repeat(), auto-fill(), and minmax() functions, as well as the CSS max(), and calc() functions. Here’s how it works.

CSS Grid’s auto-fill() function

The key to all of this is auto-fill(). We need each row to fill up with as many columns as possible. For more info on auto-fill, check out Sara Soueidan’s awesome article on the difference between auto-fill and auto-fit, which includes this helpful video showing how it works.

But how to we make sure that it doesn’t fill in too many columns?

The CSS max() function

That’s where the max() function comes in! We want each grid cell’s width to max out at a certain percentage, say 25% for a four-column grid. But, we can’t have it go below the user-specified minimum width.

So, assuming a four-column grid and minimum cell width of 100px, the max() function would look something like: max(25%, 100px).

However, the 25% value is still not quite correct because it doesn’t take the grid gaps into account. What we really need is something like this instead:

max(calc(25% - <grid-gap-for-one-cell>), 100px)

We can calc()-ulate this in CSS! (Who says CSS isn’t programming?)

--gap-count: calc(var(--grid-column-count) - 1);
--total-gap-width: calc(var(--gap-count) * var(--grid-layout-gap));
--grid-item--max-width: calc((100% - var(--total-gap-width)) / var(--grid-column-count));

Now we have another key to making this work! This will tell the grid cell to go to its maximum width — which takes into account the user-specified columns) — but will never go under 100px.

max(100px, var(--grid-item--max-width))

Learn more about the max() function with Chris Coyier’s article on the CSS min(),max(), and clamp() functions.

CSS Grid’s minmax() function

We’re getting close, but there’s one key ingredient that’s missing: The grid doesn’t always stretch to its parent’s container’s width.

This is exactly what the minmax() function is designed to do. The following CSS will set the minimum width to the <grid-item-width>, and if it has room, it’ll stretch all the cells out equally to fit the parent’s width!

minmax(<grid-item-width>, 1fr)

Let’s put it all together and make some magic!

Using the tools above, we can put together this magic bit of code that does exactly what we want!

--gap-count: calc(var(--grid-column-count) - 1);
--total-gap-width: calc(var(--gap-count) * var(--grid-layout-gap));
--grid-item--max-width: calc((100% - var(--total-gap-width)) / var(--grid-column-count));

grid-template-columns: repeat(auto-fill, minmax(max(var(--grid-item--min-width), var(--grid-item--max-width)), 1fr));

CSS is fun!

CSS has really come a long way. I had a lot of fun working on this, and I’m so happy that use-cases like this are now possible without the use of JavaScript.

Special thanks to Andy Blum, who suggested auto-fill() over auto-fit(). Also, an extremely special thanks to all of the implementors and spec writers who make advanced functions like this standardized and possible.


An Auto-Filling CSS Grid With Max Columns of a Minimum Size originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/an-auto-filling-css-grid-with-max-columns/feed/ 24 363541
Using Position Sticky With CSS Grid https://css-tricks.com/using-position-sticky-with-css-grid/ https://css-tricks.com/using-position-sticky-with-css-grid/#respond Fri, 10 Dec 2021 18:07:18 +0000 https://css-tricks.com/?p=358575 Say you’ve got a two-column CSS grid and you want one of those columns to behave like position: sticky;. There is nothing stopping you from doing that. But the default height for those two columns is going to be …


Using Position Sticky With CSS Grid originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Say you’ve got a two-column CSS grid and you want one of those columns to behave like position: sticky;. There is nothing stopping you from doing that. But the default height for those two columns is going to be “as tall as the tallest content in either column” because the default behavior for grid columns is align-items: stretch;. So, even if you have really “short” content in the column you want to behave as sticky, it won’t appear to move because really it’s already as tall as the other column.

Ahmad Shadeed makes the point that if you want that to work, you’ll probably need to align-items: start; the one you want to behave as sticky.

I would add that if you want position: sticky; behavior for elements inside either of the columns, then you’ll actually want to leave the default stretch behavior alone. Here’s an example of what I mean there as a fork:


Using Position Sticky With CSS Grid originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/using-position-sticky-with-css-grid/feed/ 0 358575
Responsive Layouts, Fewer Media Queries https://css-tricks.com/responsive-layouts-fewer-media-queries/ https://css-tricks.com/responsive-layouts-fewer-media-queries/#comments Mon, 22 Nov 2021 15:29:03 +0000 https://css-tricks.com/?p=357080 We cannot talk about web development without talking about Responsive Design. It’s just a given these days and has been for many years. Media queries are a part of Responsive Design and they aren’t going anywhere. Since the introduction of …


Responsive Layouts, Fewer Media Queries originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We cannot talk about web development without talking about Responsive Design. It’s just a given these days and has been for many years. Media queries are a part of Responsive Design and they aren’t going anywhere. Since the introduction of media queries (literally decades ago), CSS has evolved to the points that there are a lot of tricks that can help us drastically reduce the usage of media queries we use. In some cases, I will show you how to replace multiple media queries with only one CSS declaration. These approaches can result in less code, be easier to maintain, and be more tied to the content at hand.

Let’s first take a look at some widely used methods to build responsive layouts without media queries. No surprises here — these methods are related to flexbox and grid.

Using flex and flex-wrap

Live Demo

In the above demo, flex: 400px sets a base width for each element in the grid that is equal to 400px. Each element wraps to a new line if there isn’t enough room on the currently line to hold it. Meanwhile, the elements on each line grow/stretch to fill any remaining space in the container that’s leftover if the line cannot fit another 400px element, and they shrink back down as far as 400px if another 400px element can indeed squeeze in there.

Let’s also remember that flex: 400px is a shorthand equivalent to flex: 1 1 400px (flex-grow: 1, flex-shrink: 1, flex-basis: 400px).

What we have so far:

  • ✔️ Only two lines of code
  • ❌ Consistent element widths in the footer
  • ❌ Control the number of items per row
  • ❌ Control when the items wrap

Using auto-fit and minmax

Live Demo

Similar to the previous method, we are setting a base width—thanks to repeat(auto-fit, minmax(400px, 1fr))—and our items wrap if there’s enough space for them. This time, though we’re reaching for CSS Grid. That means the elements on each line also grow to fill any remaining space, but unlike the flexbox configuration, the last row maintains the same width as the rest of the elements.

So, we improved one of requirements and solved another, but also introduced a new issue since our items cannot shrink below 400px which may lead to some overflow.

  • ✔️ Only one line of code
  • ✔️ Consistent element widths in the footer
  • ❌ Control the number of items per row
  • ❌ Items grow, but do not shrink
  • ❌ Control when the items wrap

Both of the techniques we just looked at are good, but we also now see they come with a few drawbacks. But we can overcome those with some CSS trickery.

Control the number of items per row

Let’s take our first example and change flex: 400px to flex: max(400px, 100%/3 - 20px).

Resize the screen and notice that each row never has more than three items, even on a super wide screen. We have limited each line to a maximum of three elements, meaning each line only contains between one and three items at any given time.

Let’s dissect the code:

  • When the screen width increases, the width of our container also increases, meaning that 100%/3 gets bigger than 400px at some point.
  • Since we are using the max() function as the width and are dividing 100% by 3 in it, the largest any single element can be is just one-third of the overall container width. So, we get a maximum of three elements per row.
  • When the screen width is small, 400px takes the lead and we get our initial behavior.

You might also be asking: What the heck is that 20px value in the formula?

It’s twice the grid template’s gap value, which is 10px times two. When we have three items on a row, there are two gaps between elements (one on each on the left and right sides of the middle element), so for N items we should use max(400px, 100%/N - (N - 1) * gap). Yes, we need to account for the gap when defining the width, but don’t worry, we can still optimize the formula to remove it!

We can use max(400px, 100%/(N + 1) + 0.1%). The logic is: we tell the browser that each item has a width equal to 100%/(N + 1) so N + 1 items per row, but we add a tiny percentage ( 0.1%)—thus one of the items wraps and we end with only N items per row. Clever, right? No more worrying about the gap!

Now we can control the maximum number of items per row which give us a partial control over the number of items per row.

The same can also be applied to the CSS Grid method as well:

Note that here I have introduced custom properties to control the different values.

We’re getting closer!

  • ✔️ Only one line of code
  • ✔️ Consistent element widths in the footer
  • ⚠️ Partial control of the number of items per row
  • ❌ Items grow, but do not shrink
  • ❌ Control when the items wrap

Items grow, but do not shrink

We noted earlier that using the grid method could lead to overflow if the base width is bigger than the container width. To overcome this we change:

max(400px, 100%/(N + 1) + 0.1%)

…to:

clamp(100%/(N + 1) + 0.1%, 400px, 100%)

Breaking this down:

  • When the screen width is big, 400px is clamped to 100%/(N + 1) + 0.1%, maintaining our control of the maximum number of items per row.
  • When the screen width is small, 400px is clamped to 100% so our items never exceed the container width.

We’re getting even closer!

  • ✔️ Only one line of code
  • ✔️ Consistent element widths in the footer
  • ⚠️ Partial control of the number of items per row
  • ✔️ Items grow and shrink
  • ❌ Control when the items wrap

Control when the items wrap

So far, we’ve had no control over when elements wrap from one line to another. We don’t really know when it happens because it depends a number of things, like the base width, the gap, the container width, etc. To control this, we are going to change our last clamp() formula, from this:

clamp(100%/(N + 1) + 0.1%, 400px, 100%)

…to:

clamp(100%/(N + 1) + 0.1%, (400px - 100vw)*1000, 100%)

I can hear you screaming about that crazy-looking math, but bear with me. It’s easier than you might think. Here’s what’s happening:

  • When the screen width (100vw) is greater than 400px, (400px - 100vw) results in a negative value, and it gets clamped down to 100%/(N + 1) + 0.1%, which is a positive value. This gives us N items per row.
  • When the screen width (100vw) is less than 400px, (400px - 100vw) is a positive value and multiplied by a big value that’s clamped to the 100%. This results in one full-width element per row.
Live Demo

Hey, we made our first media query without a real media query! We are updating the number of items per row from N to 1 thanks to our clamp() formula. It should be noted that 400px behave as a breakpoint in this case.

What about: from N items per row to M items per row?

We can totally do that by updating our container’s clamped width:

clamp(100%/(N + 1) + 0.1%, (400px - 100vw)*1000, 100%/(M + 1) + 0.1%)

I think you probably get the trick by now. When the screen width is bigger than 400px we fall into the first rule (N items per row). When the screen width is smaller than 400px, we fall into the second rule (M items per row).

Live Demo

There we go! We can now control the number of items per row and when that number should change—using only CSS custom properties and one CSS declaration.

  • ✔️ Only one line of code
  • ✔️ Consistent element widths in the footer
  • ✔️ Full control of the number of items per row
  • ✔️ Items grow and shrink
  • ✔️ Control when the items wrap

More examples!

Controlling the number of items between two values is good, but doing it for multiple values is even better! Let’s try going from N items per row to M items per row, down to one item pre row.

Our formula becomes:

clamp(clamp(100%/(N + 1) + 0.1%, (W1 - 100vw)*1000,100%/(M + 1) + 0.1%), (W2 - 100vw)*1000, 100%)

A clamp() within a clamp()? Yes, it starts to get a big lengthy and confusing but still easy to understand. Notice the W1 and W2 variables in there. Since we are changing the number of items per rows between three values, we need two “breakpoints” (from N to M, and from M to 1).

Here’s what’s happening:

  • When the screen width is smaller than W2, we clamp to 100%, or one item per row.
  • When the screen width is larger than W2, we clamp to the first clamp().
  • In that first clamp, when the screen width is smaller than W1, we clamp to 100%/(M + 1) + 0.1%), or M items per row.
  • In that first clamp, when the screen width is bigger than W1, we clamp to 100%/(N + 1) + 0.1%), or N items per row.

We made two media queries using only one CSS declaration! Not only this, but we can adjust that declaration thanks to the CSS custom properties, which means we can have different breakpoints and a different number of columns for different containers.

How many media queries do we have in the above example? Too many to count but we will not stop there. We can have even more by nesting another clamp() to get from N columns to M columns to P columns to one column. (😱)

clamp(
  clamp(
    clamp(100%/(var(--n) + 1) + 0.1%, (var(--w1) - 100vw)*1000,
          100%/(var(--m) + 1) + 0.1%),(var(--w2) - 100vw)*1000,
          100%/(var(--p) + 1) + 0.1%),(var(--w3) - 100vw)*1000,
          100%), 1fr))
from N columns to M columns to P columns to 1 column

As I mentioned at the very beginning of this article, we have a responsive layout without any single media queries while using just one CSS declaration—sure, it’s a lengthy declaration, but still counts as one.

A small summary of what we have:

  • ✔️ Only one line of code
  • ✔️ Consistent element widths in the footer
  • ✔️ Full control of the number of items per row
  • ✔️ Items grow and shrink
  • ✔️ Control when the items wrap
  • ✔️ Easy to update using CSS custom properties

Let’s simulate container queries

Everyone is excited about container queries! What makes them neat is they consider the width of the element instead of the viewport/screen. The idea is that an element can adapt based on the width of its parent container for more fine-grain control over how elements respond to different contexts.

Container queries aren’t officially supported anywhere at the time of this writing, but we can certainly mimic them with our strategy. If we change 100vw with 100% throughout the code, things are based on the .container element’s width instead of the viewport width. As simple as that!

Resize the below containers and see the magic in play:

The number of columns change based on the container width which means we are simulating container queries! We’re basically doing that just by changing viewport units for a relative percentage value.

More tricks!

Now that we can control the number of columns, let’s explore more tricks that allow us to create conditional CSS based on either the screen size (or the element size).

Conditional background color

A while ago someone on StackOverflow asked if it is possible to change the color of an element based on its width or height. Many said that it’s impossible or that it would require a media query.

But I have found a trick to do it without a media query:

div {
  background:
   linear-gradient(green 0 0) 0 / max(0px,100px - 100%) 1px,
   red;
}
  • We have a linear gradient layer with a width equal to max(0px,100px - 100%) and a height equal to 1px. The height doesn’t really matter since the gradient repeats by default. Plus, it’s a one color gradient, so any height will do the job.
  • 100% refers to the element’s width. If 100% computes to a value bigger than 100px, the max() gives us 0px, which means that the gradient does not show, but the comma-separated red background does.
  • If 100% computes to a value smaller than 100px, the gradient does show and we get a green background instead.

In other words, we made a condition based on the width of the element compared to 100px!

This demo supports Chrome, Edge, and Firefox at the time of writing.

The same logic can be based on an element’s height instead by rearranging where that 1px value goes: 1px max(0px,100px - 100%). We can also consider the screen dimension by using vh or vw instead of %. We can even have more than two colors by adding more gradient layers.

div {
  background:
   linear-gradient(purple 0 0) 0 /max(0px,100px - 100%) 1px,
   linear-gradient(blue   0 0) 0 /max(0px,300px - 100%) 1px,
   linear-gradient(green  0 0) 0 /max(0px,500px - 100%) 1px,
   red;
}

Toggling an element’s visibility

To show/hide an element based on the screen size, we generally reach for a media query and plop a classic display: none in there. Here is another idea that simulates the same behavior, only without a media query:

div {
  max-width: clamp(0px, (100vw - 500px) * 1000, 100%);
  max-height: clamp(0px, (100vw - 500px) * 1000, 1000px);
  overflow: hidden;
}

Based on the screen width (100vw), we either get clamped to a 0px value for the max-height and max-width (meaning the element is hidden) or get clamped to 100% (meaning the element is visible and never greater than full width). We’re avoiding using a percentage for the max-height since it fails. That’s why we’re using a big pixel value (1000px).

Notice how the green elements disappear on small screens:

It should be noted that this method is not equivalent to the toggle of the display value. It’s more of a trick to give the element 0×0 dimensions, making it invisible. It may not be suitable for all cases, so use it with caution! It’s more a trick to be used with decorative elements where we won’t have accessibility issues. Chris wrote about how to hide content responsibly.

It’s important to note that I am using 0px and not 0 inside clamp() and max(). The latter makes invalidates property. I won’t dig into this but I have answered a Stack Overflow question related to this quirk if you want more detail.

Changing the position of an element

The following trick is useful when we deal with a fixed or absolutely positioned element. The difference here is that we need to update the position based on the screen width. Like the previous trick, we still rely on clamp() and a formula that looks like this: clamp(X1,(100vw - W)*1000, X2).

Basically, we are going to switch between the X1 and X2 values based on the difference, 100vw - W, where W is the width that simulates our breakpoint.

Let’s take an example. We want a div placed on the left edge (top: 50%; left:0) when the screen size is smaller than 400px, and place it somewhere else (say top: 10%; left: 40%) otherwise.

div {
  --c:(100vw - 400px); /* we define our condition */
  top: clamp(10%, var(--c) * -1000, 50%);
  left: clamp(0px, var(--c) * 1000, 40%);
}
Live Demo

First, I have defined the condition with a CSS custom property to avoid the repetition. Note that I also used it with the background color switching trick we saw earlier—we can either use (100vw - 400px) or (400px - 100vw), but pay attention to the calculation later as both don’t have the same sign.

Then, within each clamp(), we always start with the smallest value for each property. Don’t incorrectly assume that we need to put the small screen value first!

Finally, we define the sign for each condition. I picked (100vw - 400px), which means that this value will be negative when the screen width is smaller than 400px, and positive when the screen width is bigger than 400px. If I need the smallest value of clamp() to be considered below 400px then I do nothing to the sign of the condition (I keep it positive) but if I need the smallest value to be considered above 400px I need to invert the sign of the condition. That’s why you see (100vw - 400px)*-1000 with the top property.

OK, I get it. This isn’t the more straightforward concept, so let’s do the opposite reasoning and trace our steps to get a better idea of what we’re doing.

For top, we have clamp(10%,(100vw - 400px)*-1000,50%) so…

  • if the screen width (100vw) is smaller than 400px, then the difference (100vw - 400px) is a negative value. We multiply it with another big negative value (-1000 in this case) to get a big positive value that gets clamped to 50%: That means we’re left with top: 50% when the screen size is smaller than 400px.
  • if the screen width (100vw) is bigger than 400px, we end with: top: 10% instead.

The same logic applies to what we’re declaring on the left property. The only difference is that we multiply with 1000 instead of -1000 .

Here’s a secret: You don’t really need all that math. You can experiment until you get it perfect values, but for the sake of the article, I need to explain things in a way that leads to consistent behavior.

It should be noted that a trick like this works with any property that accepts length values (padding, margin, border-width, translate, etc.). We are not limited to changing the position, but other properties as well.

Demos!

Most of you are probably wondering if any of these concepts are at all practical to use in a real-world use case. Let me show you a few examples that will (hopefully) convince you that they are.

Progress bar

The background color changing trick makes for a great progress bar or any similar element where we need to show a different color based on progression.

This demo supports Chrome, Edge, and Firefox at the time of writing.

That demo is a pretty simple example where I define three ranges:

  • Red: [0% 30%]
  • Orange: [30% 60%]
  • Green: [60% 100%]

There’s no wild CSS or JavaScript to update the color. A “magic” background property allows us to have a dynamic color that changes based on computed values.

Editable content

It’s common to give users a way to edit content. We can update color based on what’s entered.

In the following example, we get a yellow “warning” when entering more than three lines of text, and a red “warning” if we go above six lines. This can a way to reduce JavaScript that needs to detect the height, then add/remove a particular class.

This demo supports Chrome, Edge, and Firefox at the time of writing.

Timeline layout

Timelines are great patterns for visualizing key moments in time. This implementation uses three tricks to get one without any media queries. One trick is updating the number of columns, another is hiding some elements on small screens, and the last one is updating the background color. Again, no media queries!

When the screen width is below 600px, all of the pseudo elements are removed, changing the layout from two columns to one column. Then the color updates from a blue/green/green/blue pattern to a blue/green/blue/green one.

Responsive card

Here’s a responsive card approach where CSS properties update based on the viewport size. Normally, we might expect the layout to transition from two columns on large screens to one column on small screens, where the card image is stacked either above or below the content. In this example, however, we change the position, width, height, padding, and border radius of the image to get a totally different layout where the image sits beside the card title.

Speech bubbles

Need some nice-looking testimonials for your product or service? These responsive speech bubbles work just about anywhere, even without media queries.

Fixed button

You know those buttons that are sometimes fixed to the left or right edge of the screen, usually for used to link up a contact for or survey? We can have one of those on large screens, then transform it into a persistent circular button fixed to the bottom-right corner on small screens for more convenient taps.

Fixed alert

One more demo, this time for something that could work for those GDPR cookie notices:

Conclusion

Media queries have been a core ingredient for responsive designs since the term responsive design was coined years ago. While they certainly aren’t going anywhere, we covered a bunch of newer CSS features and concepts that allow us to rely less often on media queries for creating responsive layouts.

We looked at flexbox and grid, clamp(), relative units, and combined them together to do all kinds of things, from changing the background of an element based on its container width, moving positions at certain screen sizes, and even mimicking as-of-yet-unreleased container queries. Exciting stuff! And all without one @media in the CSS.

The goal here is not to get rid or replace media queries but it’s more to optimize and reduce the amount of code especially that CSS has evolved a lot and now we have some powerful tool to create conditional styles. In other words, it’s awesome to see the CSS feature set grow in ways that make our lives as front-enders easier while granting us superpowers to control how our designs behave like we have never had before.


Responsive Layouts, Fewer Media Queries originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/responsive-layouts-fewer-media-queries/feed/ 20 357080
Expandable Sections Within a CSS Grid https://css-tricks.com/expandable-sections-within-a-css-grid/ https://css-tricks.com/expandable-sections-within-a-css-grid/#comments Fri, 15 Oct 2021 16:18:20 +0000 https://css-tricks.com/?p=353473 I love CSS Grid. I love how, with just a few lines of code, we can achieve fully responsive grid layouts, often without any media queries at all. I’m quite comfortable wrangling CSS Grid to produce interesting layouts, while keeping …


Expandable Sections Within a CSS Grid originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I love CSS Grid. I love how, with just a few lines of code, we can achieve fully responsive grid layouts, often without any media queries at all. I’m quite comfortable wrangling CSS Grid to produce interesting layouts, while keeping the HTML markup clean and simple.

But recently, I was presented with a unique UI conundrum to solve. Essentially, any given grid cell could have a button that would open up another, larger area that is also part of the grid. But this new larger grid cell needed to be:

  1. right below the cell that opened it, and
  2. full width.

Turns out there is a nice solution to it, and in the spirit of CSS Grid itself, it only involves a couple of lines of code. In this article, I’ll combine three one-line CSS Grid “tricks” to solve this. No JavaScript needed at all.

An explanation of the actual problem I need to solve

Here’s a minimalist UI example of what I needed to do:

This is our actual product card grid, as rendered in our Storybook component library:

A grid of product cards in a three by two layout. Each card has a placeholder gray image, product name, descriptions, price, and small text.

Each product card needed a new “quick view” button added such that, when clicked, it would:

  • dynamically “inject” a new full-width card (containing more detailed product information) immediately below the product card that was clicked,
  • without disrupting the existing card grid (i.e. retain the DOM source order and the visual order of the rendered cards in the browser), and
  • still be fully responsive.

Hmmm… was this even possible with our current CSS Grid implementation?

Surely I would need to resort to JavaScript to re-calculate the card positions, and move them around, especially on browser resize? Right?

Google was not my friend. I couldn’t find anything to help me. Even a search of “quick view” implementations only resulted in examples that used modals or overlays to render the injected card. After all, a modal is usually the only choice in situations like this, as it focuses the user on the new content, without needing to disrupt the rest of the page.

I slept on the problem, and ultimately came to a workable solution by combining some of CSS Grid’s most powerful and useful features.

CSS Grid Trick #1

I was already employing the first trick for our default grid system, and the product card grid is a specific instance of that approach. Here’s some (simplified) code:

.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, 20rem);
}

The “secret sauce” in this code is the grid-template-columns: repeat(auto-fit, 20rem); which gives us a grid with columns (20rem wide in this example) that are arranged automatically in the available space, wrapping to the next row when there’s not enough room.

Curious about auto-fit vs auto-fill? Sara Soueidan has written a wonderful explanation of how this works. Sara also explains how you can incorporate minmax() to enable the column widths to “flex” but, for the purposes of this article, I wanted to define fixed column widths for simplicity.

CSS Grid Trick #2

Next, I had to accommodate a new full-width card into the grid:

.fullwidth {
  grid-column: 1 / -1;
}

This code works because grid-template-columns in trick #1 creates an “explicit” grid, so it’s possible to define start and end columns for the .fullwidth card, where 1 / -1 means “start in column 1, and span every column up to the very last one.”

Great. A full-width card injected into the grid. But… now we have gaps above the full-width card.

Two rows of four rectangles. All of the rectangles are light gray and numbered, except one that has a wheat-colored background and another box beneath it containing text, and taking up the full container width.

CSS Grid Trick #3

Filling the gaps — I’ve done this before with a faux-masonry approach:

.grid {
  grid-auto-flow: dense;
}

That’s it! Required layout achieved.

The grid-auto-flow property controls how the CSS Grid auto-placement algorithm works. In this case, the dense packing algorithm tries to fills in holes earlier in the grid.

  • All our grid columns are the same width. Dense packing also works if the column widths are flexible, for example, by using minmax(20rem, 1f).
  • All our grid “cells” are the same height in each row. This is the default CSS Grid behavior. The grid container implicitly has align-items: stretch causing cells to occupy 100% of the available row height.

The result of all this is that the holes in our grid are filled — and the beautiful part is that the original source order is preserved in the rendered output. This is important from an accessibility perspective.

See MDN for a complete explanation of how CSS Grid auto-placement works.

The complete solution

These three combined tricks provide a simple layout solution that requires very little CSS. No media queries, and no JavaScript needed.

But… we do still need JavaScript?

Yes, we do. But not for any layout calculations. It is purely functional for managing the click events, focus state, injected card display, etc.

For demo purposes in the prototype, the full-width cards have been hard-coded in the HTML in their correct locations in the DOM, and the JavaScript simply toggles their display properties.

In a production environment, however, the injected card would probably be fetched with JavaScript and placed in the correct location. Grid layouts for something like products on an eCommerce site tend to have very heavy DOMs, and we want to avoid unnecessarily bloating the page weight further with lots of additional “hidden” content.

Quick views should be considered as a progressive enhancement, so if JavaScript fails to load, the user is simply taken to the appropriate product details page.

Accessibility considerations

I’m passionate about using correct semantic HTML markup, adding aria- properties when absolutely necessary, and ensuring the UI works with just a keyboard as well as in a screen reader.

So, here’s a rundown of the considerations that went into making this pattern as accessible as possible:

  • The product card grid uses a <ul><li> construct because we’re displaying a list of products. Assistive technologies (e.g. screen readers) will therefore understand that there’s a relationship between the cards, and users will be informed how many items are in the list.
  • The product cards themselves are <article> elements, with proper headings, etc.
  • The HTML source order is preserved for the cards when the .fullwidth card is injected, providing a good natural tab order into the injected content, and out again to the next card.
  • The whole card grid is wrapped in an aria-live region so that DOM changes are announced to screen readers.
  • Focus management ensures that the injected card receives keyboard focus, and on closing the card, keyboard focus is returned to the button that originally triggered the card’s visibility.

Although it isn’t demonstrated in the prototype, these additional enhancements could be added to any production implementation:

  • Ensure the injected card, when focused, has an appropriate label. This could be as simple as having a heading as the first element inside the content.
  • Bind the ESC key to close the injected card.
  • Scroll the browser window so that the injected card is fully visible inside the viewport.

Wrapping up

So, what do you think?

This could be a nice alternative to modals for when we want to reveal additional content, but without hijacking the entire viewport in the process. This might be interesting in other situations as well — think photo captions in an image grid, helper text, etc. It might even be an alternative to some cases where we’d normally reach for <details>/<summary> (as we know those are only best used in certain contexts).

Anyway, I’m interested in how you might use this, or even how you might approach it differently. Let me know in the comments!

Updates

Firstly, I’m really glad that this article has proved helpful to other front-end developers. I knew I couldn’t have been the only one to face a similar conundrum.

Secondly, following some constructive feedback, I’ve added a strikethrough to some of the specific accessibility considerations above, and updated my CodePen demo with the following changes:

  • There is no need for the card grid to be wrapped in an aria-live region. Instead, I have made the quick view open and close buttons behave as “toggle” buttons, with appropriate aria-expanded and aria-controls attributes. I do use this pattern for disclosure widgets (show/hide, tabs, accordions) but in this case, I was imagining a behavior more similar to a modal interface, albeit inline rather than an overlay. (Thanks to Adrian for the tip!)
  • I am no longer programatically focusing on the injected card. Instead, I simply add a tabindex="0" so a keyboard user can choose whether or not to move to the injected card, or they can simply close the “toggle” button again.
  • I still believe that using a <ul><li> construct for the grid is a suitable approach for a list of product cards. The afforded semantics indicate an explicit relationship between the cards.

Expandable Sections Within a CSS Grid originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/expandable-sections-within-a-css-grid/feed/ 40 353473
Less Absolute Positioning With Modern CSS https://css-tricks.com/less-absolute-positioning-with-modern-css/ https://css-tricks.com/less-absolute-positioning-with-modern-css/#comments Wed, 13 Oct 2021 14:42:26 +0000 https://css-tricks.com/?p=353700 Ahmad Shadeed blogs the sentiment that we might not need to lean on position: absolute as much as we might have in the past. For one thing: stacking elements. For example, if you have a stack of elements that should …


Less Absolute Positioning With Modern CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Ahmad Shadeed blogs the sentiment that we might not need to lean on position: absolute as much as we might have in the past. For one thing: stacking elements. For example, if you have a stack of elements that should all go on top of each other…

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

All the elements occupy the same grid cell at that point, but you can still use alignment and justification to move stuff around and get it looking and behaving how you want.

What you are really saying with position: absolute is I want this element to be entirely removed from the flow such that it doesn’t affect other elements and other elements don’t affect it. Sometimes you do, but arguably less often than your existing CSS muscle memory would have you believe.

I’ll snag one of Ahmad’s idea here:

Both the tag and the title are positioned in a way we might automatically think of using absolute positioning. But again, something like CSS Grid has all of the alignment features we need to not only stack them vertically, but place them right where we want.

To Shared LinkPermalink on CSS-Tricks


Less Absolute Positioning With Modern CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/less-absolute-positioning-with-modern-css/feed/ 4 353700