Tag: Exploring

Exploring CSS Grid’s Implicit Grid and Auto-Placement Powers

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 implicit 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: 2; } .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: 2; }

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. You should get the newsletter.

CSS-Tricks

, , , , ,

Exploring the CSS Paint API: Rounding Shapes

Adding borders to complex shapes is a pain, but rounding the corner of complex shapes is a nightmare! Luckily, the CSS Paint API is here to the rescue! That’s what we’re going to look at as part of this “Exploring the CSS Paint API” series.

Exploring the CSS Paint API series:


Here’s what we’re aiming for. Like everything else we’ve looked at in this series, note that only Chrome and Edge support this for now.

Live Demo

You may have noticed a pattern forming if you’ve followed along with the rest of the articles. In general, when we work with the CSS Paint API:

  • We write some basic CSS that we can easily adjust.
  • All the complex logic is done behind the scene inside the paint() function.

We can actually do this without the Paint API

There are probably a lot of ways to put rounded corners on complex shapes, but I will share with you three methods I’ve used in my own work.

I already hear you saying: If you already know three methods, then why are you using the Paint API? Good question. I’m using it because the three methods I’m aware of are difficult, and two of them are specifically related to SVG. I have nothing against SVG, but a CSS-only solution makes thing easier to maintain, plus it’s easier for someone to walk into and understand when grokking the code.

Onto those three methods…

Using clip-path: path()

If you are an SVG guru, this method is for you. the clip-path property accepts SVG paths. That means we can easily pass in the path for a complex rounded shape and be done. This approach is super easy if you already have the shape you want, but it’s unsuitable if you want an adjustable shape where, for example, you want to adjust the radius.

Below an example of a rounded hexagon shape. Good luck trying to adjust the curvature and the shape size! You’re gonna have to edit that crazy-looking path to do it.

I suppose you could refer to this illustrated guide to SVG paths that Chris put together. But it’s still going to be a lot of work to plot the points and curves just how you want it, even referencing that guide.

Using an SVG filter

I discovered this technique from Lucas Bebber’s post about creating a gooey effect. You can find all the technical details there, but the idea is to apply an SVG filter to any element to round its corners.

We simply use clip-path to create the shape we want then apply the SVG filter on a parent element. To control the radius, we adjust the stdDeviation variable.

This is a good technique, but again, it requires a deep level of SVG know-how to make adjustments on the spot.

Using Ana Tudor’s CSS-only approach

Yes, Ana Tudor found a CSS-only technique for a gooey effect that we can use to round the corner of complex shapes. She’s probably writing an article about it right now. Until then, you can refer to the slides she made where she explain how it works.

Below a demo where I am replacing the SVG filter with her technique:

Again, another neat trick! But as far as being easy to work with? Not so much here, either, especially if we’re considering more complex situations where we need transparency, images, etc. It’s work finding the correct combination of filter, mix-blend-mode and other properties to get things just right.

Using the CSS Paint API instead

Unless you have a killer CSS-only way to put rounded borders on complex shapes that you’re keeping from me (share it already!), you can probably see why I decided to reach for the CSS Paint API.

The logic behind this relies on the same code structure I used in the article covering the polygon border. I’m using the --path variable that defines our shape, the cc() function to convert our points, and a few other tricks we’ll cover along the way. I highly recommend reading that article to better understand what we’re doing here.

First, the CSS setup

We first start with a classic rectangular element and define our shape inside the --path variable (shape 2 above). The --path variable behaves the same way as the path we define inside clip-path: polygon()Use Clippy to generate it. 

.box {   display: inline-block;   height: 200px;   width: 200px;    --path: 50% 0,100% 100%,0 100%;   --radius: 20px;   -webkit-mask: paint(rounded-shape); }

Nothing complex so far. We apply the custom mask and we define both the --path and a --radius variable. The latter will be used to control the curvature.

Next, the JavaScript setup

In addition to the points defined by the path variable (pictured as red points above), we’re adding even more points (pictured as green points above) that are simply the midpoints of each segment of the shape. Then we use the arcTo() function to build the final shape (shape 4 above).

Adding the midpoints is pretty easy, but using arcTo() is a bit tricky because we have to understand how it works. According to MDN:

[It] adds a circular arc to the current sub-path, using the given control points and radius. The arc is automatically connected to the path’s latest point with a straight line, if necessary for the specified parameters.

This method is commonly used for making rounded corners.

The fact that this method requires control points is the main reason for the extra midpoints points. It also require a radius (which we are defining as a variable called --radius).

If we continue reading MDN’s documentation:

One way to think about arcTo() is to imagine two straight segments: one from the starting point to a first control point, and another from there to a second control point. Without arcTo(), these two segments would form a sharp corner: arcTo() creates a circular arc that fits this corner and smooths is out. In other words, the arc is tangential to both segments.

Each arc/corner is built using three points. If you check the figure above, notice that for each corner we have one red point and two green points on each side. Each red-green combination creates one segment to get the two segments detailed above.

Let’s zoom into one corner to better understand what is happening:

We have both segments illustrated in black.
The circle in blue illustrates the radius.

Now imagine that we have a path that goes from the first green point to the next green point, moving around that circle. We do this for each corner and we have our rounded shape.

Here’s how that looks in code:

// We first read the variables for the path and the radius. const points = properties.get('--path').toString().split(','); const r = parseFloat(properties.get('--radius').value);  var Ppoints = []; var Cpoints = []; const w = size.width; const h = size.height; var N = points.length; var i; // Then we loop through the points to create two arrays. for (i = 0; i < N; i++) {   var j = i-1;   if(j<0) j=N-1;      var p = points[i].trim().split(/(?!(.*)s(?![^(]*?))/g);   // One defines the red points (Ppoints)   p = cc(p[0],p[1]);   Ppoints.push([p[0],p[1]]);   var pj = points[j].trim().split(/(?!(.*)s(?![^(]*?))/g);   pj = cc(pj[0],pj[1]);   // The other defines the green points (Cpoints)   Cpoints.push([p[0]-((p[0]-pj[0])/2),p[1]-((p[1]-pj[1])/2)]); }  /* ... */  // Using the arcTo() function to create the shape ctx.beginPath(); ctx.moveTo(Cpoints[0][0],Cpoints[0][1]); for (i = 0; i < (Cpoints.length - 1); i++) {   ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[i+1][0],Cpoints[i+1][1], r); } ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[0][0],Cpoints[0][1], r); ctx.closePath();  /* ... */  ctx.fillStyle = '#000'; ctx.fill();

The last step is to fill our shape with a solid color. Now we have our rounded shape and we can use it as a mask on any element.

That’s it! Now all we have to do is to build our shape and control the radius like we want — a radius that we can animate, thanks to @property which will make things more interesting!

Live Demo

Are there any drawbacks with this method?

Yes, there are drawbacks, and you probably noticed them in the last example. The first drawback is related to the hover-able area. Since we are using mask, we can still interact with the initial rectangular shape. Remember, we faced the same issue with the polygon border and we used clip-path to fix it. Unfortunately, clip-path does not help here because it also affects the rounded corner.

Let’s take the last example and add clip-path. Notice how we are losing the “inward” curvature.

There’s no issue with the hexagon and triangle shapes, but the others are missing some curves. It could be an interesting feature to keep only the outward curvature — thanks to clip-path— and at the same time we fix the hover-able area. But we cannot keep all the curvatures and reduce the hover-able area at the same time.

The second issue? It’s related to the use of a big radius value. Hover over the shapes below and see the crazy results we get:

It’s actually not a “major” drawback since we have control over the radius, but it sure would be good to avoid such a situation in case we wrongly use an overly large radius value. We could fix this by limiting the value of the radius to within a range that caps it at a maximum value. For each corner, we calculate the radius that allows us to have the biggest arc without any overflow. I won’t dig into the math logic behind this (😱), but here is the final code to cap the radius value:

var angle =  Math.atan2(Cpoints[i+1][1] - Ppoints[i][1], Cpoints[i+1][0] - Ppoints[i][0]) - Math.atan2(Cpoints[i][1]   - Ppoints[i][1], Cpoints[i][0]   - Ppoints[i][0]); if (angle < 0) {   angle += (2*Math.PI) } if (angle > Math.PI) {   angle = 2*Math.PI - angle } var distance = Math.min(   Math.sqrt(     (Cpoints[i+1][1] - Ppoints[i][1]) ** 2 +      (Cpoints[i+1][0] - Ppoints[i][0]) ** 2),   Math.sqrt(     (Cpoints[i][1] - Ppoints[i][1]) ** 2 +      (Cpoints[i][0] - Ppoints[i][0]) ** 2)   ); var rr = Math.min(distance * Math.tan(angle/2),r);

r is the radius we are defining and rr is the radius we’re actually using. It equal either to r or the maximum value allowed without overflow.

If you hover the shapes in that demo, we no longer get strange shapes but the “maximum rounded shape” (I just coined this) instead. Notice that the regular polygons (like the triangle and hexagon) logically have a circle as their “maximum rounded shape” so we can have cool transitions or animations between different shapes.

Can we have borders?

Yes! All we have to do is to use stroke() instead of fill() inside our paint() function. So, instead of using:

ctx.fillStyle = '#000'; ctx.fill();

…we use this:

ctx.lineWidth = b; ctx.strokeStyle = '#000'; ctx.stroke();

This introduces another variable, b, that controls the border’s thickness.

Did you notice that we have some strange overflow? We faced the same issue in the previous article, and that due to how stroke() works. I quoted MDN in that article and will do it again here as well:

Strokes are aligned to the center of a path; in other words, half of the stroke is drawn on the inner side, and half on the outer side.

Again, it’s that “half inner side, half outer side” that’s getting us! In order to fix it, we need to hide the outer side using another mask, the first one where we use the fill(). First, we need to introduce a conditional variable to the paint() function in order to choose if we want to draw the shape or only its border.

Here’s what we have:

if(t==0) {   ctx.fillStyle = '#000';   ctx.fill(); } else {   ctx.lineWidth = 2*b;   ctx.strokeStyle = '#000';   ctx.stroke(); }

Next, we apply the first type of mask (t=0) on the main element, and the second type (t=1) on a pseudo-element. The mask applied on the pseudo-element produces the border (the one with the overflow issue). The mask applied on the main element addresses the overflow issue by hiding the outer part of the border. And if you’re wondering, that’s why we are adding twice the border thickness to lineWidth.

Live Demo

See that? We have perfect rounded shapes as outlines and we can adjust the radius on hover. And can use any kind of background on the shape.

And we did it all with a bit of CSS:

div {   --radius: 5px; /* Defines the radius */   --border: 6px; /* Defines the border thickness */   --path: /* Define your shape here */;   --t: 0; /* The first mask on the main element */      -webkit-mask: paint(rounded-shape);   transition: --radius 1s; } div::before {   content: "";    background: ..; /* Use any background you want */   --t: 1; /* The second mask on the pseudo-element */   -webkit-mask: paint(rounded-shape); /* Remove this if you want the full shape */ } div[class]:hover {   --radius: 80px; /* Transition on hover */ }

Let’s not forget that we can easily introduce dashes using setLineDash() the same way we did in the previous article.

Live Demo

Controlling the radius

In all the examples we’ve looked at, we always consider one radius applied to all the corners of each shape. It would be interesting if we could control the radius of each corner individually, the same way the border-radius property takes up to four values. So let’s extend the --path variable to consider more parameters.

Actually, our path can be expressed as a list of [x y] values. We’ll make a list of [x y r] values where we introduce a third value for the radius. This value isn’t mandatory; if omitted, it falls back to the main radius.

.box {   display: inline-block;   height: 200px;   width: 200px;    --path: 50% 0 10px,100% 100% 5px,0 100%;   --radius: 20px;   -webkit-mask: paint(rounded-shape); }

Above, we have a 10px radius for the first corner, 5px for the second, and since we didn’t specify a value for the third corner, it inherits the 20px defined by the --radius variable.

Here’s our JavaScript for the values:

var Radius = []; // ... var p = points[i].trim().split(/(?!(.*)s(?![^(]*?))/g); if(p[2])   Radius.push(parseInt(p[2])); else   Radius.push(r);

This defines an array that stores the radius of each corner. Then, after splitting the value of each point, we test whether we have a third value (p[2]). If it’s defined, we use it; if not, we use the default radius. Later on, we’re using Radius[i] instead of r.

Live Demo

This minor addition is a nice feature for when we want to disable the radius for a specific corner of the shape. In fact, let’s look at a few different examples next.

More examples!

I made a series of demos using this trick. I recommend setting the radius to 0 to better see the shape and understand how the path is created. Remember that the --path variable behaves the same way as the path we define inside clip-path: polygon(). If you’re looking for a path to play with, try using Clippy to generate one for you.

Example 1: CSS shapes

A lot of fancy shapes can be created using this technique. Here are a few of them done without any extra elements, pseudo-elements, or hack-y code.

Example 2: Speech bubble

In the previous article, we added border to a speech bubble element. Now we can improve it and round the corners using this new method.

If you compare with this example with the original implementation, you may notice the exact same code. I simply made two or three changes to the CSS to use the new Worklet.

Example 3: Frames

Find below some cool frames for your content. No more headaches when we need gradient borders!

Simply play with the --path variable to create your own responsive frame with any coloration your want.

Example 4: Section divider

SVG is no longer needed to create those wavy section dividers that are popular these days.

Notice that the CSS is light and relatively simple. I only updated the path to generate new instances of the divider.

Example 5: Navigation menu

Here’s a classic design pattern that I’m sure many of us have bumped into at some time: How the heck do we invert the radius? You’ve likely seen it in navigation designs.

A slightly different take on it:

Example 6: Gooey effect

If we play with the path values we can reach for some fancy animation.
Below an idea where I am applying a transition to only one value of the path and yet we get a pretty cool effect

This one’s inspired by Ana Tudor’s demo.

Another idea with a different animation

Another example with a more complex animation:

What about a bouncing ball

Example 7: Shape morphing

Playing with big radius values allows us to create cool transitions between different shapes, especially between a circle and a regular polygon.

If we add some border animation, we get “breathing” shapes!

Let’s round this thing up

I hope you’ve enjoyed getting nerdy with the CSS Paint API. Throughout this series, we’ve applied paint() to a bunch of real-life examples where having the API allows us to manipulate elements in a way we’ve never been able to do with CSS — or without resorting to hacks or crazy magic numbers and whatnot. I truly believe the CSS Paint API makes seemingly complicated problems a lot easier to solve in a straightforward way and will be a feature we reach for time and again. That is, when browser support catches up to it.

If you’ve followed along with this series, or even just stumbled into this one article, I’d love to know what you think of the CSS Paint API and how you imagine using it in your work. Are there any current design trends that would benefit from it, like the wavy section dividers? Or blobby designs? Experiment and have fun!

This one’s taken from my previous article


Exploring the CSS Paint API series:


The post Exploring the CSS Paint API: Rounding Shapes appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Exploring the CSS Paint API: Polygon Border

Nowadays, creating complex shapes is an easy task using clip-path, but adding a border to the shapes is always a pain. There is no robust CSS solution and we always need to produce specific “hacky” code for each particular case. In this article, I will show you how to solve this problem using the CSS Paint API.

Exploring the CSS Paint API series:


Before we dig into this third experimentation, Here is a small overview of what we are building. And, please note that everything we’re doing here is only supported in Chromium-based browsers so you’ll want to view the demos in Chrome, Edge, or Opera. See caniuse for the latest support.

Live demo

You will find no complex CSS code there but rather a generic code where we only adjust a few variables to control the shape.

The main idea

In order to achieve the polygon border, I am going to rely on a combination of the CSS clip-path property and a custom mask created with the Paint API.

Live Demo
  1. We start with a basic rectangular shape.
  2. We apply clip-path to get our polygon shape.
  3. We apply the custom mask to get our polygon border

The CSS setup

Here’s the CSS for the clip-path step we’ll get to:

.box {   --path: 50% 0,100% 100%,0 100%;    width: 200px;   height: 200px;   background: red;   display: inline-block;   clip-path: polygon(var(--path)); }

Nothing complex so far but note the use of the CSS variable --path. The entire trick relies on that single variable. Since I will be using a clip-path and a mask, both need to use the same parameters, hence the --path variable. And, yes, the Paint API will use that same variable to create the custom mask.

The CSS code for the whole process becomes:

.box {   --path: 50% 0,100% 100%,0 100%;   --border: 5px;    width: 200px;   height: 200px;   background: red;   display: inline-block;   clip-path: polygon(var(--path));   -webkit-mask: paint(polygon-border) }

In addition to the clip-path, we apply the custom mask, plus we add an extra variable, --border, to control the thickness of the border. As you can see, everything is still pretty basic and generic CSS so far. After all, this is one of the things that makes the CSS Paint API so great to work with.

The JavaScript setup

I highly recommend reading the first part of my previous article to understand the structure of the Paint API.

Now, let’s see what is happening inside the paint() function as we jump into JavaScript:

const points = properties.get('--path').toString().split(','); const b = parseFloat(properties.get('--border').value); const w = size.width; const h = size.height;  const cc = function(x,y) {   // ... }  var p = points[0].trim().split(" "); p = cc(p[0],p[1]);  ctx.beginPath(); ctx.moveTo(p[0],p[1]); for (var i = 1; i < points.length; i++) {   p = points[i].trim().split(" ");   p = cc(p[0],p[1]);   ctx.lineTo(p[0],p[1]); } ctx.closePath();  ctx.lineWidth = 2*b; ctx.strokeStyle = '#000'; ctx.stroke();

The ability to get and set CSS custom properties is one of the reasons they’re so great. We can reach for JavaScript to first read the value of the --path variable, then convert it into an array of points (seen on the very first line above). So, that means 50% 0,100% 100%,0 100% become the points for the mask, i.e. points = ["50% 0","100% 100%","0 100%"].

Then we loop through the points to draw a polygon using moveTo and lineTo. This polygon is exactly the same as the one drawn in CSS with the clip-path property.

Finally, and after drawing the shape, I add a stroke to it. I define the thickness of the stroke using lineWidth and I set a solid color using strokeStyle. In other words, only the stroke of the shape is visible since I am not filling the shape with any color (i.e. it’s transparent).

Now all we have to do is to update the path and the thickness to create any polygon border. It’s worth noting that we are not limited to solid color here since we are using the CSS background property. We can consider gradients or images.

Live Demo

In case we need to add content, we have to consider a pseudo-element. Otherwise, the content gets clipped in the process. It’s not incredibly tough to support content. We move the mask property to the pseudo-element. We can keep the clip-path declaration on the main element.

Questions so far?

I know you probably have some burning questions you want to ask after looking over that last script. Allow me to preemptively answer a couple things I bet you have in mind.

What is that cc() function?

I am using that function to convert the value of each point into pixel values. For each point, I get both x and y coordinates — using points[i].trim().split(" ") — and then I convert those coordinates to make them usable inside the canvas element that allows us to draw with those points.

const cc = function(x,y) {   var fx=0,fy=0;   if (x.indexOf('%') > -1) {     fx = (parseFloat(x)/100)*w;   } else if(x.indexOf('px') > -1) {     fx = parseFloat(x);   }   if (y.indexOf('%') > -1) {      fy = (parseFloat(y)/100)*h;   } else if(y.indexOf('px') > -1) {     fy = parseFloat(y);   }   return [fx,fy]; }

The logic is simple: if it’s a percentage value, I use the width (or the height) to find the final value. If it’s a pixel value, I simply get the value without the unit. If, for, example we have [50% 20%] where the width is equal to 200px and the height is equal to 100px, then we get [100 20]. If it’s [20px 50px], then we get [20 50]. And so on.

Why are you using CSS clip-path if the mask is already clipping the element to the stroke of the shape?

Using only the mask was the first idea I had in mind, but I stumbled upon two major issues with that approach. The first is related to how stroke() works. From MDN:

Strokes are aligned to the center of a path; in other words, half of the stroke is drawn on the inner side, and half on the outer side.

That “half inner side, half outer side” gave me a lot of headaches, and I always end up with a strange overflow when putting everything together. That’s where CSS clip-path helps; it clips the outer part and only keeps the inner side — no more overflow!

You will notice the use of ctx.lineWidth = 2*b. I am adding double the border thickness because I will clip half of it to end with the right thickness needed around the entire shape.

The second issue is related to the shape’s hover-able area. It’s known that masking does not affect that area and we can still hover/interact with the whole rectangle. Again, reaching for clip-path fixes the issue, plus we limit the interaction just to the shape itself.

The following demo illustrates these two issues. The first element has both a mask and clip-path, while the second only has the mask. We can clearly see the overflow issue. Try to hover the second one to see that we can change the color even if the cursor is outside the triangle.

Why are you using @property with the border value?

This is an interesting — and pretty tricky — part. By default, custom properties (like --border) are considered a “CSSUnparsedValue” which means they are treated as strings. From the CSS spec:

CSSUnparsedValue’ objects represent property values that reference custom properties. They are comprised of a list of string fragments and variable references.

With @property, we can register the custom property and give it a type so that it can be recognized by the browser and handled as a valid type instead of a string. In our case, we are registering the border as a <length> type so later it becomes a CSSUnitValue. What this also does is allow us to use any length unit (px, em, ch,vh, etc.) for the border value.

This may sound a bit complex but let me try to illustrate the difference with a DevTools screenshot.

I am using console.log() on a variable where I defined 5em. The first one is registered but the second one is not.

In the first case, the browser recognizes the type and makes the conversion into a pixel value, which is useful since we only need pixel values inside the paint() function. In the second case, we get the variable as a string which is not very useful since we cannot convert em units into px units inside the paint() function.

Try all the units. It will always results with the computed pixel value inside the paint() function.

What about the --path variable?

I wanted to use the same approach with the --path variable but, unfortunately, I think I pushed CSS right up to the limits of what it can do here. Using @property, we can register complex types, even multi-value variables. But that’s still not enough for the path we need.

We can use the + and # symbols to define a space-separated or comma-separated list of values, but our path is a comma-separated list of space-separated percentage (or length) values. I would use something like [<length-percentage>+]#, but it doesn’t exist.

For the path, I am obliged to manipulate it as a string value. That limits us just to percentage and pixel values for now. For this reason, I defined the cc() function to convert the string values into pixel values.

We can read in the CSS spec:

The internal grammar of the syntax strings is a subset of the CSS Value Definition Syntax. Future levels of the specification are expected to expand the complexity of the allowed grammar, allowing custom properties that more closely resemble the full breadth of what CSS properties allow.

Even if the grammar is extend to be able to register the path, we will still face issue in case we need to include calc() inside our path:

--path: 0 0,calc(100% - 40px) 0,100% 40px,100% 100%,0 100%;

In the above, calc(100% - 40px) is a value that the browser considers a <length-percentage>, but the browser cannot compute that value until it knows the reference for the percentage. In other words, we cannot get the equivalent pixel value inside the paint() function since the reference can only be known when the value gets used within var().

To overcome this, we can can extend the cc() function to do the conversion. We did the conversion of a percentage value and a pixel value, so let’s combine those into one conversion. We will consider 2 cases: calc(P% - Xpx) and calc(P% + Xpx). Our script becomes:

const cc = function(x,y) {    var fx=0,fy=0;   if (x.indexOf('calc') > -1) {     var tmp = x.replace('calc(','').replace(')','');     if (tmp.indexOf('+') > -1) {       tmp = tmp.split('+');       fx = (parseFloat(tmp[0])/100)*w + parseFloat(tmp[1]);     } else {       tmp = tmp.split('-');       fx = (parseFloat(tmp[0])/100)*w - parseFloat(tmp[1]);     }    } else if (x.indexOf('%') > -1) {       fx = (parseFloat(x)/100)*w;    } else if(x.indexOf('px') > -1) {       fx = parseFloat(x);    }           if (y.indexOf('calc') > -1) {     var tmp = y.replace('calc(','').replace(')','');     if (tmp.indexOf('+') > -1) {        tmp = tmp.split('+');        fy = (parseFloat(tmp[0])/100)*h + parseFloat(tmp[1]);      } else {        tmp = tmp.split('-');        fy = (parseFloat(tmp[0])/100)*h - parseFloat(tmp[1]);      }     } else if (y.indexOf('%') > -1) {       fy = (parseFloat(y)/100)*h;     } else if(y.indexOf('px') > -1) {       fy = parseFloat(y);     }   return [fx,fy]; }

We’re using indexOf() to test the existence of calc, then, with some string manipulation, we extract both values and find the final pixel value.

And, as a result, we also need to update this line:

p = points[i].trim().split(" ");

…to:

p = points[i].trim().split(/(?!(.*)s(?![^(]*?))/g);

Since we need to consider calc(), using the space character won’t work for splitting. That’s because calc() also contains spaces. So we need a regex. Don’t ask me about it — it’s the one that worked after trying a lot from Stack Overflow.

Here is basic demo to illustrate the update we did so far to support calc()

Notice that we have stored the calc() expression within the variable --v that we registered as a <length-percentage>. This is also a part of the trick because if we do this, the browser uses the correct format. Whatever the complexity of the calc() expression, the browser always converts it to the format calc(P% +/- Xpx). For this reason, we only have to deal with that format inside the paint() function.

Below different examples where we are using a different calc() expression for each one:

If you inspect the code of each box and see the computed value of --v, you will always find the same format which is super useful because we can have any kind of calculation we want.

It should be noted that using the variable --v is not mandatory. We can include the calc() directly inside the path. We simply need to make sure we insert the correct format since the browser will not handle it for us (remember that we cannot register the path variable so it’s a string for the browser). This can be useful when we need to have many calc() inside the path and creating a variable for each one will make the code too lengthy. We will see a few examples at the end.

Can we have dashed border?

We can! And it only takes one instruction. The <canvas> element already has a built-in function to draw dashed stroke setLineDash():

The setLineDash() method of the Canvas 2D API’s CanvasRenderingContext2D interface sets the line dash pattern used when stroking lines. It uses an array of values that specify alternating lengths of lines and gaps which describe the pattern.

All we have to do is to introduce another variable to define our dash pattern.

Live Demo

In the CSS, we simply added a CSS variable, --dash, and within the mask is the following:

// ... const d = properties.get('--dash').toString().split(','); // ... ctx.setLineDash(d);

We can also control the offset using lineDashOffset. We will see later how controlling the offset can help us reach some cool animations.

Why not use @property instead to register the dash variable?

Technically, we can register the dash variable as a <length># since it’s a comma-separated list of length values. It does work, but I wasn’t able to retrieve the values inside the paint() function. I don’t know if it’s a bug, a lack of support, or I’m just missing a piece of the puzzle.

Here is a demo to illustrate the issue:

I am registering the --dash variable using this:

@property --dash{   syntax: '<length>#';   inherits: true;   initial-value: 0; }

…and later declaring the variable as this:

--dash: 10em,3em;

If we inspect the element, we can see that the browser is handling the variable correctly since the computed values are pixel ones

But we only get the first value inside the paint() function

Until I find the a fix for this, I am stuck using the --dash variable as a string, like the --path. Not a big deal in this case as I don’t think we will need more than pixel values.

Use cases!

After exploring the behind the scene of this technique, let’s now focus on the CSS part and check out a few uses cases for our polygon border.

A collection of buttons

We can easily generate custom shape buttons having cool hover effect.

Notice how calc() is used inside the path of the last button the way we described it earlier. It works fine since I am following the correct format.

Breadcrumbs

No more headaches when creating a breadcrumb system! Below, you will find no “hacky” or complex CSS code, but rather something that’s pretty generic and easy to understand where all we have to do is to adjust a few variables.

Card reveal animation

If we apply some animation to the thickness, we can get some fancy hover effect

We can use that same idea to create an animation that reveals the card:

Callout & speech bubble

“How the hell we can add border to that small arrow???” I think everyone has stumbled on this issue when dealing with either a callout or speech bubble sort of design. The Paint API makes this trivial.

In that demo, you will find a few examples that you can extend. You only need to find the path for your speech bubble, then adjust some variables to control the border thickness and the size/position of the arrow.

Animating dashes

A last one before we end. This time we will focus on the dashed border to create more animations. We already did one in the button collection where we transform a dashed border into a solid one. Let’s tackle two others.

Hover the below and see the nice effect we get:

Those who have worked with SVG for some time are likely familiar with the sort effect that we achieve by animating stroke-dasharray. Chris even tackled the concept a while back. Thanks to the Paint API, we can do this directly in CSS. The idea is almost the same one we use with SVG. We define the dash variable:

--dash: var(--a),1000;

The variable --a starts at 0, so our pattern is a solid line (where the length equals 0) with a gap (where length 1000); hence no border. We animate --a to a big value to draw our border.

We also talked about using lineDashOffset, which we can use for another kind of animation. Hover the below and see the result:

Finally, a CSS solution to animate the position of dashes that works with any kind of shape!

What I did is pretty simple. I added an extra variable, --offset, to which I apply a transition from 0 to N. Then, inside the paint() function, I do the following:

const o = properties.get('--offset'); ctx.lineDashOffset=o;

As simple as that! Let’s not forget an infinite animation using keyframes:

We can make the animation run continuously by offsetting 0 to N where N is the sum of the values used in the dash variable (which, in our case, is 10+15=25). We use a negative value to have the opposite direction direction.

I have probably missed a lot of use cases that I let you discover!


Exploring the CSS Paint API series:


The post Exploring the CSS Paint API: Polygon Border appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Exploring the CSS Paint API: Blob Animation

After the fragmentation effect, I am going to tackle another interesting animation: the blob! We all agree that such effect is hard to achieve with CSS, so we generally reach for SVG to make those gooey shapes. But now that the powerful Paint API is available, using CSS is not only possible, but maybe even a preferable approach once browser support comes around.

Here’s what we’re making. It’s just Chrome and Edge support for now, so check this out on one of those browsers as we go along.

Live demo (Chrome and Edge only)

Building the blob

Let’s understand the logic behind drawing a blob using a classic <canvas> element to better illustrate the shape:

When talking about a blob, we’re also talking about the general shape of a distorted circle, so that’s the shape we can use as our base. We define N points that we place around a circle (illustrated in green).

const CenterX = 200; const CenterY = 200; const Radius = 150; const N = 10; var point = [];  for (var i = 0; i < N; i++) {   var x = Math.cos((i / N) * (2 * Math.PI)) * Radius + CenterX;   var y = Math.sin((i / N) * (2 * Math.PI)) * Radius + CenterY;   point[i] = [x, y]; }

Considering the center point (defined by CenterX/CenterY) and the radius, we calculate the coordinate of each point using some basic trigonometry.

After that, we draw a cubic Bézier curve between our points using quadraticCurveTo(). To do this, we introduce more points (illustrated in red) because a cubic Bézier curve requires a start point, a control point, and an end point.

The red points are the start and end points, and the green points can be the control points. Each red point is placed at the midpoint between two green points.

ctx.beginPath(); /* start the path */ var xc1 = (point[0][0] + point[N - 1][0]) / 2; var yc1 = (point[0][1] + point[N - 1][1]) / 2; ctx.moveTo(xc1, yc1); for (var i = 0; i < N - 1; i++) {   var xc = (point[i][0] + point[i + 1][0]) / 2;   var yc = (point[i][1] + point[i + 1][1]) / 2;   ctx.quadraticCurveTo(point[i][0], point[i][1], xc, yc); } ctx.quadraticCurveTo(point[N - 1][0], point[N - 1][1], xc1, yc1); ctx.closePath(); /* end the path */

Now all we have to do is to update the position of our control points to create the blob shape. Let’s try with one point by adding the following:

point[3][0]= Math.cos((3 / N) * (2 * Math.PI)) * (Radius - 50) + CenterX; point[3][1]= Math.sin((3 / N) * (2 * Math.PI)) * (Radius - 50) + CenterY;

The third point is closest to the center of our circle (by about 50px) and our cubic Bézier curve follows the movement perfectly to keep a curved shape.

Let’s do the same with all the points. We can use the same general idea, changing these existing lines:

var x = Math.cos((i / N) * (2 * Math.PI)) * Radius + CenterX; var y = Math.sin((i / N) * (2 * Math.PI)) * Radius + CenterY;

…into:

var r = 50*Math.random(); var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - r) + CenterX; var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - r) + CenterY;

Each point is offset by a random value between 0 and 50 pixels, bringing each point closer to the center by a slightly different amount. And we get our blob shape as a result!

Now we apply that shape as a mask on an image using the CSS Paint API. Since we are dealing with a blobby shape, it’s suitable to consider square elements (height equal to width) instead, where the radius is equal to half the width or height.

Here we go using a CSS variable (N) to control the number of points.

I highly recommend reading the first part of my previous article to understand the structure of the Paint API.

Each time the code runs, we get a new shape, thanks to the random configuration.

Let’s animate this!

Drawing a blog is good and all, but animating it is better! Animating the blob is actually the main purpose of this article, after all. We will see how to create different kinds of gooey blob animations using the same foundation of code.

The main idea is to smoothly adjust the position of the points — whether it’s all or some of them — to transition between two shapes. Let’s start with the basic one: a transition from a circle into a blob by changing the position of one point.

Animated gif shoeing a cursor hovering the right edge of a circular image. The right side of the image caves in toward the center of the shape on hover, and returns when the cursor leaves the shape.
Live demo (Chrome and Edge only)

For this, I introduced a new CSS variable, B , to which I am applying a CSS transition.

@property --b{   syntax: '<number>';   inherits: false;   initial-value: 0; } img {   --b:0;   transition:--b .5s; } img:hover {   --b:100 }

I get the value of this variable inside the paint() function and use it to define the position of our point.

If you check the code in the embedded linked demo, you will notice this:

if(i==0)    var r = RADIUS - B; else   var r = RADIUS

All the points have a fixed position (defined by the shape’s radius) but the first point specifically has a variable position, (RADIUS - B ). On hover, The value of B is changing from 0 to 100, moving our point closer to the middle while creating that cool effect.

Let’s do this for more points. Not all of them but only the even ones. I will define the position as follow:

var r = RADIUS - B*(i%2);
An animated gif showing a cursor hovering over a circular image. The shape of the image morphs to a sort of star-like shape when the cursor enters the image, then returns when the cursor leaves.
Live demo (Chrome and Edge only)

We have our first blob animation! We defined 20 points and are making half of them closer to the center on hover.

We can easily have different blobby variations simply by adjusting the CSS variables. We define the number of points and the final value of the B variable.

Live demo (Chrome and Edge only)

Now let’s try it with some random stuff. Instead of moving our points with a fixed value, let’s make that value random move them all around. We previously used this:

var r = RADIUS - B*(i%2);

Let’s change that to this:

var r = RADIUS - B*random();

…where random() gives us a value in the range [0 1]. In other words, each point is moved by a random value between 0 and B . Here’s what we get:

Live demo (Chrome and Edge only)

See that? We get another cool animation with the same code structure. We only changed one instruction. We can make that instruction a variable so we can decide if we want to use the uniform or the random configuration without changing our JavaScript. We introduce another variable, T, that behaves like a boolean:

if(T == 0)    var r = RADIUS - B*(i%2); else    var r = RADIUS - B*random();

We have two animations and, thanks to the T variable, we get to decide which one to use. We can control the number of points using N and the distance using the variable V. Yes, a lot of variables but don’t worry, we will sum up everything at the end.

What is that random() function doing?

It’s the same function I used in the previous article. We saw there that we cannot rely on the default built-in function because we need a random function where we are able to control the seed to make sure we always get the same sequence of random values. So the seed value is also another variable that we can control to get a different blob shape. Go change that value manually and see the result.

In the previous article, I mentioned that the Paint API removes all of the complexity on the CSS side of things, and that gives us more flexibility to create complex animations. For example, we can combine what we have done up to this point with keyframes and cubic-bezier():

Live demo (Chrome and Edge only)

The demo includes another example using the parabolic curve I detailed in a previous article.

Controlling the movement of the points

In all the blobs we’ve created so far, we considered the same movement for our points. Whether we’re using the uniform configuration or the random one, we always move the points from the edge to the center of the circle following a line.

Now let’s see how we can control that movement in order to get even more animations. The idea behind this logic is simple: we move the x and y differently.

Previously we were doing this:

var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - F(B)) + CenterX; var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - F(B)) + CenterY;

…where F(B) is a function based on the variable B that holds the transition.

Now we will have something like this instead:

var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - Fx(B)) + CenterX; var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - Fy(B)) + CenterY;

…where we’ve updating the x and y variables differently to makes more animations. Let’s try a few.

One axis movement

For this one, we will make one of the functions equal to 0 and keep the other one the same as before. In other words, one coordinate remains fixed along the animation

If we do:

Fy(B) = 0

…we get:

A cursor hovers two circular images, which has points that pull inward from the left and right edges of the circle to created jagged sides along the circles.
Live demo (Chrome and Edge only)

The points are only moving horizontally to get another kind of effect. We can easily do the same for the other axis by making Fx(B)=0 (see a demo).

I think you get the idea. All we have to do is to adjust the functions for each axis to get a different animation.

Left or right movement

Let’s try another kind of movement. Instead of making the points converging into the center, let’s make them move into the same direction (either right or left). We need a condition based on the location of the point which is defined by the angle.

Illustration showing the blue outline of a circle with 8 points around the shape and thick red lines bisecting the circle to show the axes.

We have two group of points: ones in the [90deg 270deg] range (the left side), and the remaining points along the ride side of the shape. If we consider the indexes, we can express the range differently, like [0.25N 0.75N] where N is the number of points.

The trick is to have a different sign for each group:

var sign = 1; if(i<0.75*N && i>0.25*N)    sign = -1; /* we invert the sign for the left group */ if(T == 0)    var r = RADIUS - B*sign*(i%2); else    var r = RADIUS - B*sign*random(); var x = Math.cos((i / N) * (2 * Math.PI)) * r + cx;

And we get:

An animated gif showing a cursor entering the right side of a circular image, which has points the are pulled toward the center of the circle, then return once the cursor exits.
Live demo (Chrome and Edge only)

We are able to get the same direction but with one small drawback: one group of the points are going outside the mask area because we are increasing the distance on some points while decreasing the distance on others. We need to reduce the size of our circle to leave enough space for all of our points.

We simply decrease the size of our circle using the V value that defines the final value for our B variable. In other words, it’s the maximum distance that one point can reach.

Our initial shape (illustrated by the grey area and defined with the green points) will cover a smaller area since we will decrease the Radius value with the value of V:

const V = parseFloat(properties.get('--v')); const RADIUS = size.width/2 - V;
Live demo (Chrome and Edge only)

We fixed the issue of the points getting outside but we have another small drawback: the hover-able area is the same, so the effect starts even before the cursor hits the image. It would be good if we can also reduce that area so everything is consistent.

We can use an extra wrapper and a negative margin trick. Here’s the demo. The trick is pretty simple:

.box {   display: inline-block;   border-radius: 50%;   cursor: pointer;   margin: calc(var(--v) * 1px);   --t: 0; }  img {   display: block;   margin: calc(var(--v) * -1px);   pointer-events: none;   -webkit-mask: paint(blob);   --b: 0;   transition:--b .5s; } .box:hover img {   --b: var(--v) }

The extra wrapper is an inline-block element. The image inside it has negative margins equal to the V variable which reduces the overall size of the shape’s box. Then we disable the hover effect on the image element (using pointer-events: none) so only the box element triggers the transition. Finally we add some margin to the box element to avoid any overlap.

Like the previous effect, this one can also be combined with cubic-bezier() and keyframes to get more cool animations. Below is an example using my sinusoidal curve for a wobbling effect on hover.

Live demo (Chrome and Edge only)

If we add some transforms, we can create a kind of strange (but pretty cool) sliding animation:

A circular image is distorted and slides from right to left on hover in this animated gif.
Live demo (Chrome and Edge only)

Circular movement

Let’s tackle another interesting movement that will allow us to create infinite and “realistic” blob animations. Instead of moving our points from one location to another, we will rotate them around an orbit to have a continuous movement.

The blue outline of a circle with ten green points along its edges. A gray arrow shows the circle's radius and assigns it to a point on the circle with a variable, r, that has a red circle around it showing its hover boundary.

The initial location of our points (in green) will become an orbit and the red circle is the path that our points will take. In other words, each point will rotate around its initial position following a circle having a radius r.

All we need to do is make sure there is no overlap between two adjacent paths so the radius need to have a maximum allowed value.

I will not detail the math but the max value is equal to:

const r = 2*Radius*Math.sin(Math.PI/(2*N));
A blobby shape moves in a circular motion around an image of a cougar's face.
Live demo (Chrome and Edge only)

This is the relevant part of the code:

var r = (size.width)*Math.sin(Math.PI/(N*2)); const RADIUS = size.width/2 - r; // ...  for(var i = 0; i < N; i++) {   var rr = r*random();   var xx = rr*Math.cos(B * (2 * Math.PI));   var yy = rr*Math.sin(B * (2 * Math.PI));    var x = Math.cos((i / N) * (2 * Math.PI)) * RADIUS + xx + cx;   var y = Math.sin((i / N) * (2 * Math.PI)) * RADIUS + yy + cy;   point[i] = [x,y]; }

We get the max value of the radius and we reduce that value from the main radius. Remember that we need to have enough space for our points so we need to reduce the mask area like we did with the previous animation. Then for each point we get a random radius rr (between 0 and r). Then we calculate the position inside the circular path using xx and yy. And, finally, we place the path around its orbit and get the final position (the x, y values).

Notice the value B which is, as usual, the one with the transition. This time, we will have a transition from 0 to 1 in order to make a full turn around the orbit.

Spiral movement

One more for you! This one is a combination of the two previous ones.

We saw how to move the points around a fixed orbit and how to move a point from the edge of the circle to the center. We can combine both and have our point move around an orbit and we do the same for the orbit by moving it from the edge to the center.

Let’s add an extra variable to our existing code:

for(var i = 0; i < N; i++) {   var rr = r*random();   var xx = rr*Math.cos(B * (2 * Math.PI));   var yy = rr*Math.sin(B * (2 * Math.PI));     var ro = RADIUS - Bo*random();   var x = Math.cos((i / N) * (2 * Math.PI)) * ro + xx + cx;   var y = Math.sin((i / N) * (2 * Math.PI)) * ro + yy + cy;   point[i] = [x,y]; }

As you can see, I used the exact same logic as the very first animation we looked at. We reduce the radius with a random value (controlled with Bo in this case).

A blob morphs shape as it moves around an image of a cougar's face.
Live demo (Chrome and Edge only)

Yet another fancy blob animation! Now each element has two animations: one animates the orbit (Bo), and the other animates the point in its circular path (B). Imagine all the effects that you can get by simply adjusting the animation value (duration, ease, etc.)!

Putting everything together

Oof, we are done with all the animations! I know that some of you may have gotten lost with all the variations and all the variables we introduced, but no worries! We will sum everything up right now and you will see that it’s easier than what might expect.

I want to also highlight that what I have done is not an exhaustive list of all the possible animations. I only tackled a few of them. We can define even more but the main purpose of this article is to understand the overall structure and be able to extend it as needed.

Let’s summarize what we have done and what are the main points:

  • The number of points (N): This variable is the one that controls the granularity of the blob’s shape. We define it in the CSS and it is later used to define the number of control points.
  • The type of movement (T): In almost all the animations we looked at, I always considered two kind of animations: a “uniform” animation and a “random” one. I am calling this the type of movement that we can control using the variable T set in the CSS. We will have somewhere in the code to do an if-else based on that T variable.
  • The random configuration: When dealing with random movement, we need to use our own random() function where we can control the seed in order to have the same random sequence for each element. The seed can also be considered a variable, one that generates different shapes.
  • The nature of movement: This is the path that the points take. We can have a lot of variations, for example:
    • From the edge of the circle to the center
    • A one axis movement (the x or y axis)
    • A circular movement
    • A spiral movement
    • and many others…

Like the type of movement, the nature of the movement can also be made conditional by introducing another variable, and there is no limit to what can be done here. All we have to do is to find the math formula to create another animation.

  • The animation variable (B): This is the CSS variable that contains the transition/animation. We generally apply a transition/animation from 0 to a certain value (defined in all the examples with the variable V). This variable is used to express the position of our points. Updating that variable logically updates the positions; hence the animations we get. In most cases, we only need to animate one variable, but we can have more based on the nature of the movement (like the spiral one where we used two variables).
  • The shape area: By default, our shape covers the entire element area, but we saw that some movement require the points to go outside the shape. That’s why we had to reduce the area. We generally do this by the maximum value of B (defined by V), or a different value based on the nature of the movement.

Our code is structured like this:

var point = [];  /* The center of the element */ const cx = size.width/2; const cy = size.height/2; /* We read all of the CSS variables */ const N = parseInt(properties.get('--n')); /* number of points */ const T = parseInt(properties.get('--t')); /* type of movement  */ const Na = parseInt(properties.get('--na')); /* nature of movement  */ const B = parseFloat(properties.get('--b')); /* animation variable */ const V = parseInt(properties.get('--v'));  /* max value of B */ const seed = parseInt(properties.get('--seed')); /* the seed */ // ...  /* The radius of the shape */ const RADIUS = size.width/2 - A(V,T,Na);  /* Our random() function */ let random =  function() {   // ... } /* we define the position of our points */ for(var i = 0; i < N; i++) {    var x = Fx[N,T,Na](B) + cx;    var y = Fy[N,T,Na](B) + cy;    point[i] = [x,y]; }  /* We draw the shape, this part is always the same */ ctx.beginPath(); // ... ctx.closePath(); /* We fill it with a solid color */ ctx.fillStyle = '#000'; ctx.fill();

As you can see, the code is not as complex as you might have expected. All the work is within those function Fx and Fy, which defines the movement based on N,T and Na. We also have the function A that reduces the size of the shape to prevent points overflowing the shape during the animation.

Let’s check the CSS:

@property --b {   syntax: '<number>';   inherits: false;   initial-value: 0; }  img {   -webkit-mask:paint(blob);   --n: 20;   --t: 0;   --na: 1;   --v: 50;   --seed: 125;   --b: 0;   transition: --b .5s; } img:hover {   --b: var(--v); }

I think the code is self-explanatory. You define the variables, apply the mask, and animate the B variable using either a transition or keyframes. That’s all!

I will end this article with a final demo where I put all the variations together. All you have to do is to play with the CSS variables


Exploring the CSS Paint API series:


The post Exploring the CSS Paint API: Blob Animation appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Exploring the CSS Paint API: Image Fragmentation Effect

In my previous article, I created a fragmentation effect using CSS mask and custom properties. It was a neat effect but it has one drawback: it uses a lot of CSS code (generated using Sass). This time I am going to redo the same effect but rely on the new Paint API. This drastically reduces the amount of CSS and completely removes the need for Sass.

Here is what we are making. Like in the previous article, only Chrome and Edge support this for now.

See that? No more than five CSS declarations and yet we get a pretty cool hover animation.

What is the Paint API?

The Paint API is part of the Houdini project. Yes, “Houdini” the strange term that everyone is talking about. A lot of articles already cover the theoretical aspect of it, so I won’t bother you with more. If I have to sum it up in a few words, I would simply say : it’s the future of CSS. The Paint API (and the other APIs that fall under the Houdini umbrella) allow us to extend CSS with our own functionalities. We no longer need to wait for the release of new features because we can do it ourselves!

From the specification:

An API for allowing web developers to define a custom CSS <image> with javascript [sic], which will respond to style and size changes.

And from the explainer:

The CSS Paint API is being developed to improve the extensibility of CSS. Specifically this allows developers to write a paint function which allows us to draw directly into an elements [sic] background, border, or content.

I think the idea is pretty clear. We can draw what we want. Let’s start with a very basic demo of background coloration:

  1. We add the paint worklet using CSS.paintWorklet.addModule('your_js_file').
  2. We register a new paint method called draw.
  3. Inside that, we create a paint() function where we do all the work. And guess what? Everything is like working with <canvas>. That ctx is the 2D context, and I simply used some well-known functions to draw a red rectangle covering the whole area.

This may look unintuitive at first glance, but notice that the main structure is always the same: the three steps above are the “copy/paste” part that you repeat for each project. The real work is the code we write inside the paint() function.

Let’s add a variable:

As you can see, the logic is pretty simple. We define the getter inputProperties with our variables as an array. We add properties as a third parameter to paint() and later we get our variable using properties.get().

That’s it! Now we have everything we need to build our complex fragmentation effect.

Building the mask

You may wonder why the paint API to create a fragmentation effect. We said it’s a tool to draw images so how it will allow us to fragment an image?

In the previous article, I did the effect using different mask layer where each one is a square defined with a gradient (remember that a gradient is an image) so we got a kind of matrix and the trick was to adjust the alpha channel of each one individually.

This time, instead of using many gradients we will define only one custom image for our mask and that custom image will be handled by our paint API.

An example please!

In the above, I have created an image having an opaque color covering the left part and a semi-transparent one covering the right part. Applying this image as a mask gives us the logical result of a half-transparent image.

Now all we need to do is to split our image to more parts. Let’s define two variables and update our code:

The relevant part of the code is the following:

const n = properties.get('--f-n'); const m = properties.get('--f-m');  const w = size.width/n; const h = size.height/m;  for(var i=0;i<n;i++) {   for(var j=0;j<m;j++) {     ctx.fillStyle = 'rgba(0,0,0,'+(Math.random())+')';         ctx.fillRect(i*w, j*h, w, h); } }

N and M define the dimension of our matrix of rectangles. W and H are the size of each rectangle. Then we have a basic FOR loop to fill each rectangle with a random transparent color.

With a little JavaScript, we get a custom mask that we can easily control by adjusting the CSS variables:

Now, we need to control the alpha channel in order to create the fading effect of each rectangle and build the fragmentation effect.

Let’s introduce a third variable that we use for the alpha channel that we also change on hover.

We defined a CSS custom property as a <number> that we transition from 1 to 0, and that same property is used to define the alpha channel of our rectangles. Nothing fancy will happen on hover because all the rectangles will fade the same way.

We need a trick to prevent fading of all the rectangles at the same time, instead creating a delay between them. Here is an illustration to explain the idea I am going to use:

The above is showing the alpha animation for two rectangles. First we define a variable L that should be bigger or equal to 1 then for each rectangle of our matrix (i.e. for each alpha channel) we perform a transition between X and Y where X - Y = L so we have the same overall duration for all the alpha channel. X should be bigger or equal to 1 and Y smaller or equal to 0.

Wait, the alpha value shouldn’t be in the range [1 0], right ?

Yes, it should! And all the tricks that we’re working on rely on that. Above, the alpha is animating from 8 to -2, meaning we have an opaque color in the [8 1] range, a transparent one in the [0 -2] range and an animation within [1 0]. In other words, any value bigger than 1 will have the same effect as 1, and any value smaller than 0 will have the same effect as 0.

Animation within [1 0] will not happen at the same time for both our rectangles. Rectangle 2 will reach [1 0] before Rectangle 1 will. We apply this to all the alpha channels to get our delayed animations.

In our code we will update this:

rgba(0,0,0,'+(o)+') 

…to this:

rgba(0,0,0,'+((Math.random()*(l-1) + 1) - (1-o)*l)+') 

L is the variable illustrated previously, and O is the value of our CSS variable that transitions from 1 to 0

When O=1, we have (Math.random()*(l-1) + 1). Considering the fact that the random() function gives us a value within the [0 1] range, the final value will be in the [L 1]range.

When O=0, we have (Math.random()*(l-1) + 1 - l) and a value with the [0 1-L] range.

L is our variable to control the delay.

Let’s see this in action:

We are getting closer. We have a cool fragmentation effect but not the one we saw in the beginning of the article. This one isn’t as smooth.

The issue is related the random() function. We said that each alpha channel need to animate between X and Y, so logically those value need to remain the same. But the paint() function is called a bunch during the transition, so each time, the random() function give us different X and Y values for each alpha channel; hence the “random” effect we are getting.

To fix this we need to find a way to store the generated value so they are always the same for each call of the paint() function. Let’s consider a pseudo-random function, a function that always generates the same sequence of values. In other words, we want to control the seed.

Unfortunately, we cannot do this with the JavaScript’s built-in random() function, so like any good developer, let’s pick one up from Stack Overflow:

const mask = 0xffffffff; const seed = 30; /* update this to change the generated sequence */ let m_w  = (123456789 + seed) & mask; let m_z  = (987654321 - seed) & mask;  let random =  function() {   m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;   m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;   var result = ((m_z << 16) + (m_w & 65535)) >>> 0;   result /= 4294967296;   return result; }

And the result becomes:

We have our fragmentation effect without complex code:

  • a basic nested loop to create NxM rectangles
  • a clever formula for the channel alpha to create the transition delay
  • a ready random() function taken from the Net

That’s it! All you have to do is to apply the mask property to any element and adjust the CSS variables.

Fighting the gaps!

If you play with the above demos you will notice, in some particular case, strange gaps between the rectangles

To avoid this, we can extend the area of each rectangle with a small offset.

We update this:

ctx.fillRect(i*w, j*h, w, h); 

…with this:

ctx.fillRect(i*w-.5, j*h-.5, w+.5, h+.5); 

It creates a small overlap between the rectangles that compensates for the gaps between them. There is no particular logic with the value 0.5 I used. You can go bigger or smaller based on your use case.

Want more shapes?

Can the above be extended to consider more than rectangular shape? Sure it can! Let’s not forget that we can use Canvas to draw any kind of shape — unlike pure CSS shapes where we sometimes need some hacky code. Let’s try to build that triangular fragmentation effect.

After searching the web, I found something called Delaunay triangulation. I won’t go into the deep theory behind it, but it’s an algorithm for a set of points to draw connected triangles with specific properties. There are lots of ready-to-use implementations of it, but we’ll go with Delaunator because it’s supposed to be the fastest of the bunch.

We first define a set of points (we will use random() here) then run Delauntor to generate the triangles for us. In this case, we only need one variable that defines the number of points.

const n = properties.get('--f-n'); const o = properties.get('--f-o'); const w = size.width; const h = size.height; const l = 7;   var dots = [[0,0],[0,w],[h,0],[w,h]]; /* we always include the corners */ /* we generate N random points within the area of the element */ for (var i = 0; i < n; i++) {   dots.push([random() * w, random() * h]); } /**/ /* We call Delaunator to generate the triangles*/ var delaunay = Delaunator.from(dots); var triangles = delaunay.triangles; /**/ for (var i = 0; i < triangles.length; i += 3) { /* we loop the triangles points */   /* we draw the path of the triangles */   ctx.beginPath();   ctx.moveTo(dots[triangles[i]][0]    , dots[triangles[i]][1]);   ctx.lineTo(dots[triangles[i + 1]][0], dots[triangles[i + 1]][1]);   ctx.lineTo(dots[triangles[i + 2]][0], dots[triangles[i + 2]][1]);     ctx.closePath();   /**/   var alpha = (random()*(l-1) + 1) - (1-o)*l; /* the alpha value */   /* we fill the area of triangle with the semi-transparent color */   ctx.fillStyle = 'rgba(0,0,0,'+alpha+')';   /* we consider stroke to fight the gaps */   ctx.strokeStyle = 'rgba(0,0,0,'+alpha+')';   ctx.stroke();   ctx.fill(); } 

I have nothing more to add to the comments in the above code. I simply used some basic JavaScript and Canvas stuff and yet we have a pretty cool effect.

We can make even more shapes! All we have to do is to find an algorithm for it.

I cannot move on without doing the hexagon one!

I took the code from this article written by Izan Pérez Cosano. Our variable is now R that will define the dimension of one hexagon.

What’s next?

Now that we have built our fragmentation effect, let’s focus on the CSS. Notice that the effect is as simple as changing the opacity value (or the value of whichever property you are working with) of an element on it hover state.

Opacity animation

img {   opacity:1;   transition:opacity 1s; }  img:hover {   opacity:0; }

Fragmentation effect

img {   -webkit-mask: paint(fragmentation);   --f-o:1;   transition:--f-o 1s; }  img:hover {   --f-o:0; }

This means we can easily integrate this kind of effect to create more complex animations. Here are a bunch of ideas!

Responsive image slider

Another version of the same slider:

Noise effect

Loading screen

Card hover effect

That’s a wrap

And all of this is just the tip of the iceberg of what can be achieved using the Paint API. I’ll end with two important points:

  • The Paint API is 90% <canvas>, so the more you know about <canvas>, the more fancy things you can do. Canvas is widely used, which means there’s a bunch of documentation and writing about it to get you up to speed. Hey, here’s one right here on CSS-Tricks!
  • The Paint API removes all the complexity from the CSS side of things. There’s no dealing with complex and hacky code to draw cool stuff. This makes CSS code so much easier to maintain, not to mention less prone to error.

The post Exploring the CSS Paint API: Image Fragmentation Effect appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Exploring color-contrast() for the first time.

I saw in the release notes for Safari Technical Preview 122 that it has support for a color-contrast() function in CSS. Safari is first out of the gate here. As far as I know, no other browser supports this yet and I have no idea when stable Safari will ship it, or if any other browser ever will. But! It’s a very good idea! Any native tool to get us to ship more accessible interfaces (of which color contrast is a part) is cool by me. So let’s attempt to get it to work.

Anybody can download Safari Technical Preview, so I did that.

I had to ask around about this, but just because this is a pre-release browser, it doesn’t mean all these features are active by default. Just as Chrome Canary has feature flags you have to turn on, so does Safari Technical Preview. So, I had to flip it on like this:

The release notes don’t have any information about how to actually use color-contrast(), but fortunately web searching turns up a spec (it’s part of Color Module 5), and MDN has a page for it with very basic syntax information.

This is how I understand it:

That example above is a little silly, because it will always return white — that has the most contrast with black. This function actually gets useful when one or more of those color values is dynamic, meaning very likely it is a CSS custom property.

The function returns a color, so the top use-case, I would guess, is going to be setting a color based on a dynamic background. So…

section {   background: var(--bg);   color: color-contrast(var(--bg), white, black); }

Now we can toss any color at all at --bg and we’ll either get white or black text, depending on what has the most contrast:

That’s extremely cool, even in the most basic use case.

Here’s a demo from Dave where he’s not just setting the text color in the parent, but the color of links as well, and the links have a different set of colors to choose from. Play with the HSL sliders (in Safari Technology Preview, of course) to see it work.

Just picking two colors that have enough contrast is easy enough (although you’d be surprised how often it’s screwed up by even those of us with good intentions). But oh wow does it get complicated quick with different situations, let alone having a bunch of color variations, or god forbid, arbitrary combinations.

Here’s a video of me playing with Dave’s tester so you can see how the colors update at different places.


The post Exploring color-contrast() for the first time. appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

Exploring @property and its Animating Powers

Uh, what’s @property? It’s a new CSS feature! It gives you superpowers. No joke, there is stuff that @property can do that unlocks things in CSS we’ve never been able to do before.

While everything about @property is exciting, perhaps the most interesting thing is that it provides a way to specify a type for custom CSS properties. A type provides more contextual information to the browser, and that results in something cool: We can give the browser the information is needs to transition and animate those properties!

But before we get too giddy about this, it’s worth noting that support isn’t quite there. As it current stands at the time of this writing, @property is supported in Chrome and, by extension, Edge. We need to keep an eye on browser support for when we get to use this in other places, like Firefox and Safari.

First off, we get type checking

@property --spinAngle {   /* An initial value for our custom property */   initial-value: 0deg;   /* Whether it inherits from parent set values or not */   inherits: false;   /* The type. Yes, the type. You thought TypeScript was cool */   syntax: '<angle>'; }  @keyframes spin {   to {     --spinAngle: 360deg;   } }

That’s right! Type checking in CSS. It’s sorta like creating our very own mini CSS specification. And that’s a simple example. Check out all of the various types we have at our disposal:

  • length
  • number
  • percentage
  • length-percentage
  • color
  • image
  • url
  • integer
  • angle
  • time
  • resolution
  • transform-list
  • transform-function
  • custom-ident (a custom identifier string)

Before any of this, we may have relied on using “tricks” for powering animations with custom properties.

What cool stuff can we do then? Let’s take a look to spark our imaginations.

Let’s animate color

How might you animate an element either through a series of colors or between them? I’m a big advocate for the HSL color space which breaks things down into fairly understandable numbers: hue, saturation, and lightness, respectively.

Animating a hue feels like something fun we can do. What’s colorful? A rainbow! There’s a variety of ways we could make a rainbow. Here’s one:

In this example, CSS Custom Properties are set on the different bands of the rainbow using :nth-child() to scope them to individual bands. Each band also has an --index set to help with sizing.

To animate those bands, we might use that --index to set some negative animation delays, but then use the same keyframe animation to cycle through hues.

.rainbow__band {   border-color: hsl(var(--hue, 10), 80%, 50%);   animation: rainbow 2s calc(var(--index, 0) * -0.2s) infinite linear; }  @keyframes rainbow {   0%, 100% {     --hue: 10;   }   14% {     --hue: 35;   }   28% {     --hue: 55;   }   42% {     --hue: 110;   }   56% {     --hue: 200;   }   70% {     --hue: 230;   }   84% {     --hue: 280;   } }

That might work out okay if you want a “stepped” effect. But, those keyframe steps aren’t particularly accurate. I’ve used steps of 14% as a rough jump.

We could animate the border-color and that would get the job done. But, we’d still have a keyframe step calculation issue. And we need to write a lot of CSS to get this done:

@keyframes rainbow {   0%, 100% {     border-color: hsl(10, 80%, 50%);   }   14% {     border-color: hsl(35, 80%, 50%);   }   28% {     border-color: hsl(55, 80%, 50%);   }   42% {     border-color: hsl(110, 80%, 50%);   }   56% {     border-color: hsl(200, 80%, 50%);   }   70% {     border-color: hsl(230, 80%, 50%);   }   84% {     border-color: hsl(280, 80%, 50%);   } }

Enter @property. Let’s start by defining a custom property for hue. This tells the browser our custom property, --hue, is going to be a number (not a string that looks like a number):

@property --hue {   initial-value: 0;   inherits: false;   syntax: '<number>'; }

Hue values in HSL can go from 0 to 360. We start with an initial value of 0. The value isn’t going to inherit. And our value, in this case, is a number. The animation is as straightforward as:

@keyframes rainbow {   to {     --hue: 360;   } }

Yep, that’s the ticket:

To get the starting points accurate, we could play with delays for each band. This gives us some cool flexibility. For example, we can up the animation-duration and we get a slow cycle. Have a play with the speed in this demo.

It may not be the “wildest” of examples, but I think animating color has some fun opportunities when we use color spaces that make logical use of numbers. Animating through the color wheel before required some trickiness. For example, generating keyframes with a preprocessor, like Stylus:

@keyframes party    for $  frame in (0..100)     {$  frame * 1%}       background 'hsl(%s, 65%, 40%)' % ($  frame * 3.6)

We do this purely because this isn’t understood by the browser. It sees going from 0 to 360 on the color wheel as an instant transition because both hsl values show the same color.

@keyframes party {   from {     background: hsl(0, 80%, 50%);    }   to {     background: hsl(360, 80%, 50%);   } }

The keyframes are the same, so the browser assumes the animation stays at the same background value when what we actually want is for the browser to go through the entire hue spectrum, starting at one value and ending at that same value after it goes through the motions.

Think of all the other opportunities we have here. We can:

  • animate the saturation
  • use different easings
  • animate the lightness
  • Try rgb()
  • Try degrees in hsl() and declare our custom property type as <angle>

What’s neat is that we can share that animated value across elements with scoping! Consider this button. The border and shadow animate through the color wheel on hover.

Animating color leads me think… wow!

Straight-up numbering

Because we can define types for numbers—like integer and number—that means we can also animate numbers instead of using those numbers as part of something else. Carter Li actually wrote an article on this right here on CSS-Tricks. The trick is to use an integer in combination with CSS counters. This is similar to how we can work the counter in “Pure CSS” games like this one.

The use of counter and pseudo-elements provides a way to convert a number to a string. Then we can use that string for the content of a pseudo-element. Here are the important bits:

@property --milliseconds {   inherits: false;   initial-value: 0;   syntax: '<integer>'; }  .counter {   counter-reset: ms var(--milliseconds);   animation: count 1s steps(100) infinite; }  .counter:after {   content: counter(ms); }  @keyframes count {   to {     --milliseconds: 100;   } }

Which gives us something like this. Pretty cool.

Take that a little further and you’ve got yourself a working stopwatch made with nothing but CSS and HTML. Click the buttons! The rad thing here is that this actually works as a timer. It won’t suffer from drift. In some ways it may be more accurate than the JavaScript solutions we often reach for such as setInterval. Check out this great video from Google Chrome Developer about JavaScript counters.

What other things could you use animated numbers for? A countdown perhaps?

Animated gradients

You know the ones, linear, radial, and conic. Ever been in a spot where you wanted to transition or animate the color stops? Well, @property can do that!

Consider a gradient where we‘re creating some waves on a beach. Once we’ve layered up some images we could make something like this.

body {   background-image:     linear-gradient(transparent 0 calc(35% + (var(--wave) * 0.5)), var(--wave-four) calc(75% + var(--wave)) 100%),     linear-gradient(transparent 0 calc(35% + (var(--wave) * 0.5)), var(--wave-three) calc(50% + var(--wave)) calc(75% + var(--wave))),     linear-gradient(transparent 0 calc(20% + (var(--wave) * 0.5)), var(--wave-two) calc(35% + var(--wave)) calc(50% + var(--wave))),     linear-gradient(transparent 0 calc(15% + (var(--wave) * 0.5)), var(--wave-one) calc(25% + var(--wave)) calc(35% + var(--wave))), var(--sand); }

There is quite a bit going on there. But, to break it down, we’re creating each color stop with calc(). And in that calculation, we add the value of --wave. The neat trick here is that when we animate that --wave value, all the wave layers move.

This is all the code we needed to make that happen:

body {   animation: waves 5s infinite ease-in-out; } @keyframes waves {   50% {     --wave: 25%;   } }

Without the use of @property, our waves would step between high and low tide. But, with it, we get a nice chilled effect like this.

It’s exciting to think other neat opportunities that we get when manipulating images. Like rotation. Or how about animating the angle of a conic-gradient… but, within a border-image. Bramus Van Damme does a brilliant job covering this concept.

Let’s break it down by creating a charging indicator. We’re going to animate an angle and a hue at the same time. We can start with two custom properties:

@property --angle {   initial-value: 0deg;   inherits: false;   syntax: '<number>'; }  @property --hue {   initial-value: 0;   inherits: false;   syntax: '<angle>'; }

The animation will update the angle and hue with a slight pause on each iteration.

@keyframes load {   0%, 10% {     --angle: 0deg;     --hue: 0;   }   100% {     --angle: 360deg;     --hue: 100;   } }

Now let’s apply it as the border-image of an element.

.loader {   --charge: hsl(var(--hue), 80%, 50%);   border-image: conic-gradient(var(--charge) var(--angle), transparent calc(var(--angle) * 0.5deg)) 30;   animation: load 2s infinite ease-in-out; }

Pretty cool.

Unfortunately, border-image doesn‘t play nice with border-radius. But, we could use a pseudo-element behind it. Combine it with the number animation tricks from before and we’ve got a full charging/loading animation. (Yep, it changes when it gets to 100%.)

Transforms are cool, too

One issue with animating transforms is transitioning between certain parts. It often ends up breaking or not looking how it should. Consider the classic example of a ball being throw. We want it to go from point A to point B while imitating the effect of gravity.

An initial attempt might look like this

@keyframes throw {   0% {     transform: translate(-500%, 0);   }   50% {     transform: translate(0, -250%);   }   100% {     transform: translate(500%, 0);   } }

But, we’ll soon see that it doesn’t look anything like we want.

Before, we may have reached for wrapper elements and animated them in isolation. But, with @property, we can animate the individual values of the transform. And all on one timeline. Let’s flip the way this works by defining custom properties and then setting a transform on the ball.

@property --x {   inherits: false;   initial-value: 0%;   syntax: '<percentage>'; }  @property --y {   inherits: false;   initial-value: 0%;   syntax: '<percentage>'; }  @property --rotate {   inherits: false;   initial-value: 0deg;   syntax: '<angle>'; }  .ball {   animation: throw 1s infinite alternate ease-in-out;   transform: translateX(var(--x)) translateY(var(--y)) rotate(var(--rotate)); }

Now for our animation, we can compose the transform we want against the keyframes:

@keyframes throw {   0% {     --x: -500%;     --rotate: 0deg;   }   50% {     --y: -250%;   }   100% {     --x: 500%;     --rotate: 360deg;   } }

The result? The curved path we had hoped for. And we can make that look different depending on the different timing functions we use. We could split the animation into three ways and use different timing functions. That would give us different results for the way the ball moves.

Consider another example where we have a car that we want to drive around a square with rounded corners.

We can use a similar approach to what we did with the ball:

@property --x {   inherits: false;   initial-value: -22.5;   syntax: '<number>'; }  @property --y {   inherits: false;   initial-value: 0;   syntax: '<number>'; }  @property --r {   inherits: false;   initial-value: 0deg;   syntax: '<angle>'; }

The car’s transform is using calculated with vmin to keep things responsive:

.car {   transform: translate(calc(var(--x) * 1vmin), calc(var(--y) * 1vmin)) rotate(var(--r)); }

Now can write an extremely accurate frame-by-frame journey for the car. We could start with the value of --x.

@keyframes journey {   0%, 100% {     --x: -22.5;   }   25% {     --x: 0;   }   50% {     --x: 22.5;   }   75% {     --x: 0;   } }

The car makes the right journey on the x-axis.

Then we build upon that by adding the travel for the y-axis:

@keyframes journey {   0%, 100% {     --x: -22.5;     --y: 0;   }   25% {     --x: 0;     --y: -22.5;   }   50% {     --x: 22.5;     --y: 0;   }   75% {     --x: 0;     --y: 22.5;   } }

Well, that’s not quite right.

Let’s drop some extra steps into our @keyframes to smooth things out:

@keyframes journey {   0%, 100% {     --x: -22.5;     --y: 0;   }   12.5% {     --x: -22.5;     --y: -22.5;   }   25% {     --x: 0;     --y: -22.5;   }   37.5% {     --y: -22.5;     --x: 22.5;   }   50% {     --x: 22.5;     --y: 0;   }   62.5% {     --x: 22.5;     --y: 22.5;   }   75% {     --x: 0;     --y: 22.5;   }   87.5% {     --x: -22.5;     --y: 22.5;   } }

Ah, much better now:

All that‘s left is the car‘s rotation. We‘re going with a 5% window around the corners. It’s not precise but it definitely shows the potential of what’s possible:

@keyframes journey {   0% {     --x: -22.5;     --y: 0;     --r: 0deg;   }   10% {     --r: 0deg;   }   12.5% {     --x: -22.5;     --y: -22.5;   }   15% {     --r: 90deg;   }   25% {     --x: 0;     --y: -22.5;   }   35% {     --r: 90deg;   }   37.5% {     --y: -22.5;     --x: 22.5;   }   40% {     --r: 180deg;   }   50% {     --x: 22.5;     --y: 0;   }   60% {     --r: 180deg;   }   62.5% {     --x: 22.5;     --y: 22.5;   }   65% {     --r: 270deg;   }   75% {     --x: 0;     --y: 22.5;   }   85% {     --r: 270deg;   }   87.5% {     --x: -22.5;     --y: 22.5;   }   90% {     --r: 360deg;   }   100% {     --x: -22.5;     --y: 0;     --r: 360deg;   } }

And there we have it, a car driving around a curved square! No wrappers, no need for complex Math. And we composed it all with custom properties.

Powering an entire scene with variables

We‘ve seen some pretty neat @property possibilities so far, but putting everything we’ve looked at here together can take things to another level. For example, we can power entire scenes with just a few custom properties.

Consider the following concept for a 404 page. Two registered properties power the different moving parts. We have a moving gradient that’s clipped with -webkit-background-clip. The shadow moves by reading the values of the properties. And we swing another element for the light effect.

That’s it!

It’s exciting to think about what types of things we can do with the ability to define types with @property. By giving the browser additional context about a custom property, we can go nuts in ways we couldn’t before with basic strings.

What ideas do you have for the other types? Time and resolution would make for interesting transitions, though I’ll admit I wasn’t able to make them work that way I was hoping. url could also be neat, like perhaps transitioning between a range of sources the way an image carousel typically does. Just brainstorming here!

I hope this quick look at @property inspires you to go check it out and make your own awesome demos! I look forward to seeing what you make. In fact, please share them with me here in the comments!


The post Exploring @property and its Animating Powers appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

Exploring the Complexities of Width and Height in CSS

The following article is co-authored by Uri Shaked and Michal Porag.

Let’s explore the complexities of how CSS computes the width and height dimensions of elements. This is based on countless late-night hours debugging and fiddling with lots of combinations of CSS properties, reading though the specs, and trying to figure out why some things seem to behave one way or another.

But before jumping in, let’s cover some basics so we’re all on the same page.

The basics

You have an element, and you want it to be 640px wide and 360px tall. These are just arbitrary numbers that conform to 16:9 pixel ratio. You can set them explicitly like this:

.element {   width: 640px;   height: 360px; }

Now, the design calls for some padding inside that element. So you modify the CSS:

.element {   width: 640px;   height: 360px;   padding: 10px; }

What is the rendered width and height of the element now? I bet you can guess… it’s not 640×360px anymore! It’s actually 660×380px, because the padding adds 10px to each side (i.e. top, right, bottom and left), for an additional 20px on both the height and width.

This has to do with the box-sizing property: if it’s set to content-box (the default value), the rendered size of the element is the width and height plus the padding and border. That might mean that the rendered size is bigger than we intend which is funny because it might wind up that an element’s declared width and height values are completely different than what’s rendered.

That’s the power of The CSS Box Model. It calculates width and height like so:

/* Width */ width + padding-left + padding-right + border-left + border-right  /* Height */ height + padding-top + padding-bottom + border-top + border-bottom

What we just saw is how the dimensions for a block element are computed. Block elements include any element that naturally takes up the full width that’s available. So, by nature, it doesn’t matter how much content the element contains because its width is always 100%, that is, until we alter it. Think of elements like <p>, <article>, <main>, <div>, and so many more.

But now we ought to look at inline elements because they behave differently when it comes to The Box Model and how their dimensions are computed. After that, we’ll look at the relationship between parent and child elements, and how they affect the width and height computations of each other.

The curious case of inline elements

As we just saw, the padding and border of any element are both included in the element’s computed width and height dimensions. Funny enough, there are cases where the width and height properties have no effect whatsoever. Such is the case when working with inline elements.

An inline element is an element that’s width and height are determined by the content it contains. Inline elements, such as a <span>, will completely ignore the width and height as well the the left and right margin properties because, well, the content is what determines the dimensions. Here, sometimes a visual can help.

Just look at how nesting a block element inside of an inline element breaks the inline element’s shape simply because the block element is not defined by the amount of content it contains, but rather the amount of available space. You can really see that in action when we add a border to the inline element. Look how the inline element abruptly stops where the paragraph comes in, then continues after the paragraph.

The span sees the paragraph, which interrupts the inline flow of the span and essentially breaks out of it. Fascinating stuff!

But there’s more! Look how the inline element completely overlooks width and margin, even when those are declared right on it.

Crazy!

Parent and child elements

The parent-child relationship is a common pattern. A parent element is one that contains other elements nested inside it. And those nested elements are the parent element’s children.

<!-- The parent element --> <div class="parent">   <!-- The children -->   <div class="child"></div>   <div class="another-child"></div>   <div class="twins">Whoa!</div> </div>

The width and height of an element gets super interesting with parent and child elements. Let’s look at all the interesting quirks we get with this.

Relative units

Let’s start with relative units. A relative unit is computed by its context, or relation to other elements. Yeah, that’s sort of a convoluted definition. There are several different types of relative units, but let’s take percentages as an example. We can say that an element is 100% wide.

<div class="child">   <!-- nothing yet --> </div>
.parent {   width: 100%; }

Cool. If we plop that element onto an empty page, it will take up 100% of the available horizontal space. And what that 100% computes to depends on the width of the browser, right? 100% of a browser that’s 1,500 pixels is 1,500 pixels wide. 100% of a browser that’s 500 pixels is 500 pixels wide. That’s what we mean by a relative unit. The actual computed value is determined by the context in which it’s used.

So, the astute reader may already be thinking: Hey, so that’s sort of like a child element that’s set to a parent element’s width. And that would be correct. The width of the child at 100% will compute based on the actual width of the parent element that contains it.

Height works much the same way: it’s relative to the parent’s height. For example, two parent elements with different height dimensions but identical children result in children with different heights.

Padding and margin

The width and height of parent-child combinations get even more interesting when we look at other properties, such as padding and margin. Apparently, when we specify a percentage value for padding or margin, it is always relative to the width of the parent, even when dealing with vertical edges.

Some clever designers have taken advantage of it to create boxes of equal width and height, or boxes that keep a certain aspect ratio when the page resizes. This is particularly useful for video or image content, but can also be (ab)used in creative ways. Go ahead, type whatever you want into the editable element in this demo. The box maintains a proportional height and width, no matter how much (or little) content is added.

This technique for creating aspect ratio boxes is lovingly referred to as the “padding hack.” Chris has covered it extensively. But now that we have the aspect-ratio property gaining wide browser support, there’s less reason to reach for it.

display: inline and inline-block

Now that we’ve taken looks at how parent and child element dimensions are computed, we should check out two other interesting property values that affect an element’s width: min-content and max-content.

These properties tell the browser to look at the content of the element in order to determine its width. For instance, if we have the text: “hello CSS encyclopedia, nice to meet you!”, the browser would calculate the space that text would take up on the screen, and use it as the width.

The difference between min-content and max-content lies in how the browser does this calculation. For max-content, the browser pretends it has infinite space, and lays all the text in a single line while measuring its width.

For min-content, the browser pretends it has zero space, so it puts every word / child inline element in a different line. Let’s see this in action:

We actually saw max-content in action when we looked at the difference between block and inline elements. Inline elements, remember, are only as wide and tall as the content they contain. We can make most elements inline elements just by declaring display: inline; on it.

Cool. Another weapon we have is display: inline-block;. That creates an inline element, but enhanced with block-level computations in The Box Model. In other words, it’s an inline element that respects margin, width and height. The best of both worlds!

Cyclic percentage size

Did that last point make sense? Well, hopefully I won’t confuse you with this:

The child element in this example has a relative width of 33%. The parent element does not have a width declared on it. How the heck is the child’s computed width get calculated when there’s nothing relative to it?

To answer that, we have to look at how the browser calculates the size of the elements in this example. We haven’t defined a specific width for the parent element, so the browser uses the initial value for width , which is auto. And since the parent element’s display is set to inline-block, auto behaves like max-content. And max-content, as we saw, should mean the parent element is as wide as the content in it, which is everything inside the child element.

So, the browser looks at the element’s content (children) to determine its width. However, the width of the child also depends on the parent’s width! Gah, this is weird!

The CSS Box Sizing Module specification calls this cyclic percentage sizing. I’m not sure why it’s called that exactly, but it details the complex math the browser has to do to (1) determine the parent element’s width, and (2) reconcile that width to the relative width of the child.

The process is actually pretty cool once you get over the math stuff. The browser starts by calculating a temporary width for the child before its declared value is applied. The temporary width the browser uses for the child is auto which we saw behaves like max-content which, in turn, tells the browser that the child needs to be as wide as the content it contains. And right now, that’s not the declared 33% value.

That max-content value is what the browser uses to calculate the parent’s width. The parent, you see, needs to be at least as wide as the content that it contains, which is everything in the child at max-content. Once that resolves, the browser goes back to the child element and applies the 33% value that’s declared in the CSS.

This is how it looks:

There! Now we know how a child element can contribute to the computed value of its parent.

M&Ms: the min- and max- properties

Hey, so you’re probably aware that the following properties exist:

  • min-width
  • min-height
  • max-width
  • max-height

Well, those have a lot to do with an element’s width and height as well. They specify the limits an element’s size. It’s like saying, Hey, browser, make sure this element is never under this width/height or above this width/height.

So, even if we have declared an explicit width on an element, say 100%, we can still cap that value by giving it a max-width:

element {   width: 100%;   max-width: 800px; }

This allows the browser to let the element take up as much space as it wants, up to 800 pixels. Let’s look what happens if we flip those values around and set the max-width to 100% and width to 800px:

element {   width: 800px;   max-width: 100%; }

Hey look, it seems to result in the exact same behavior! The element takes up all the space it needs until it gets to 800 pixels.

Apparently, things start to get more complex as soon as we add a parent element into the mix. This is the same example as above, with one notable change: now each element is a child of an inline-block element. Suddenly, we see a striking difference between the two examples:

Why the difference? To understand, let’s consider how the browser calculates the width of the elements in this example.

We start with the parent element (.parent). It has a display property set to inline-block, and we didn’t specify a width value for it. Just like before, the browser looks at the size of its children to determine its width. Each box in this example is wrapped in the .parent element.

The first box (#container1) has a percentage width value of 100%. The width of the parent resolves to the width of the text within (the child’s max-content), limited by the value we specified by max-width, and that is used to calculate the width of the child as well.

The second box (#container2) has a set width of 800px, so its parent width is also 800px — just wide enough to fit its child. Then, the child’s max-width is resolved relative to the parent’s final width, that is 800px. So both the parent and the child are 800px wide in this case.

So, even though we initially saw the two boxes behave the same when we swapped width and max-width values, we now know that isn’t always true. In this case, introducing a parent element set to display: inline-block; threw it all off!

Adding min(), max() and clamp() into the mix

The min(), max() and clamp() are three useful CSS functions that let us define the size of elements responsively… without media queries!

  • min(): Returns the minimum value of its arguments. The arguments can be given in different units, and we can even mix and match absolute and relative units, like min(800px, 100%).
  • max(): Returns the maximum value of its arguments. Just like min(), you can mix and match different units.
  • clamp(): A shorthand function for doing min and max at once: clamp(MIN, VAL, MAX) is resolved as max(MIN, min(VAL, MAX)). In other words, it will return VAL, unless it exceeds the boundaries defined by MIN and MAX, in which case it’ll return the corresponding boundary value.

Like this. Check out how we can effectively “re-write” the max-width example from above using a single CSS property:

.element {   width: min(800px, 100%); }  /* ...is equivalent to: */ .element {   width: 800px;   max-width: 100%; }

That would set the width of the element to 800px, but make sure we don’t exceed the width of the parent (100%). Just like before, if we wrap the element with an inline-block parent, we can observe it behaving differently than the max-width variation:

The width of the children (800px) is the same. However, if you enlarge the screen (or use CodePen’s 0.5x button to zoom out), you will notice that the second parent is actually larger.

It boils down to how the browser calculates the parent’s width: we didn’t specify a width for the parent, and as child’s width value is using relative units, the browser ignores it while calculating the parent’s width and uses the max-content child of the child, dictated by the “very long … long” text.

Wrapping up

Phew! It’s crazy that something as seemingly simple as width and height actually have a lot going on. Sure, we can set explicit width and height values on an element, but the actual values that render often end up being something completely different.

That’s the beauty (and, frankly, the frustration) with CSS. It’s so carefully considered and accounts for so many edge cases. I mean, the concept of The Box Model itself is wonderfully complex and elegant at the same time. Where else can we explicitly declare something in code and have it interpreted in different ways? The width isn’t always the width.

And we haven’t even touched on some other contributing factors to an element’s dimensions. Modern layout techniques, like CSS Flexbox and Grid introduce axes and track lines that also determine the rendered size of an element.


Authors: Uri Shaked and Michal Porag


The post Exploring the Complexities of Width and Height in CSS appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

Exploring What the Details and Summary Elements Can Do

Gosh bless the

element. Toss some content inside it and you have an accessible expand-for-more interaction with just about zero work.

See the Pen Simple details. by Chris Coyier (@chriscoyier) on CodePen.

Toss a

in there to customize what the expander text says.

See the Pen Multiple Details/Summary by Chris Coyier (@chriscoyier) on CodePen.

Works great for FAQs.

There is really no limit to how you can style them. If you don’t like the default focus ring, you can remove that, but make sure to put some kind of styling back.

Here I’ve used a header element for each expandable section, which has a focus state that mimics other interactive elements on the page.

The only browser that doesn’t support this are the Microsoft ones (and Opera Mini which makes sense—it doesn’t really do interactive).

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

Chrome Firefox IE Edge Safari
12 49 No 79 6

Mobile / Tablet

Android Chrome Android Firefox Android iOS Safari
86 82 4 6.0-6.1

But even then, it’s just like all the sections are opened, so it’s not a huge deal:

Wanna style that default triangle? Strangely enough, the standard way to do that is through the list-style properties. But Blink-based browsers haven’t caught up to that yet, so they have a proprietary way to do it. They can be combined though. Here’s an example of replacing it with an image:

summary {   list-style-image: url(right-arrow.svg); }  summary::-webkit-details-marker {   background: url(right-arrow.svg);   color: transparent; }

See the Pen Custom Markers on Details/Summary by Chris Coyier (@chriscoyier) on CodePen.

Unfortunately, they don’t turn, and there is no way to animate the default triangle either. One idea might be to target the :focus state and swap backgrounds:

See the Pen Custom Markers on Details/Summary by Geoff Graham (@geoffgraham) on CodePen.

But that seems to be limited to WebKit and Blink and, even then, the arrow will return once the item is out of focus even if the item is still expanded.


The post Exploring What the Details and Summary Elements Can Do appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]