CSS Triangles, Multiple Ways

I like Adam Laki’s Quick Tip: CSS Triangles because it covers that ubiquitous fact about front-end techniques: there are always many ways to do the same thing. In this case, drawing a triangle can be done:

  • with border and a collapsed element
  • with clip-path: polygon()
  • with transform: rotate() and overflow: hidden
  • with glyphs like ▼

I’d say that the way I’ve typically done triangles the most over the years is with the border trick, but I think my favorite way now is using clip-path. Code like this is fairly clear, understandable, and maintainable to me: clip-path: polygon(50% 0, 0 100%, 100% 100%); Brain: Middle top! Bottom right! Bottom left! Triangle!

My 2nd Place method goes to an option that didn’t make Adam’s list: inline <svg>! This kind of thing is nearly just as brain-friendly: <polygon points="0,0 100,0 50,100"/>.

Direct Link to ArticlePermalink

The post CSS Triangles, Multiple Ways appeared first on CSS-Tricks.

Slice and Dice a Disc with CSS

I recently came across an interesting sliced disc design. The disc had a diagonal gradient and was split into horizontal slices, offset a bit from left to right. Naturally, I started to think what would the most efficient way of doing it with CSS be.

Screenshot. Shows a diagonal gradient disc that has been split into eight horizontal slices, one on top of the other, with tiny gaps in between them and slightly offset to the left or right (with respect to the vertical axis of the disc) based on parity.
Sliced gradient disc.

The first thought was that this should be doable with border-radius, right? Well, no! The thing with border-radius is that it creates an elliptical corner whose ends are tangent to the edges it joins.

My second thought was to use a circle() clipping path. Well, turns out this solution works like a charm, so let’s take a close look at it!

Note that the following demos won’t work in Edge as Edge doesn’t yet support clip-path on HTML elements. It could all be emulated with nested elements with overflow: hidden in order to have cross-browser support, but, for simplicity, we dissect the clip-path method in this article.

Slicing a disc into equal parts

As far as the HTML structure goes, we generate it with a preprocessor to avoid repetition. First off, we decide upon a number of slices n. Then we pass this number to the CSS as a custom property --n. Finally, we generate the slices in a loop, passing the index of each to the CSS as another custom property --i.

- var n = 8; style :root { --n: #{n} } - for(var i = 0; i < n; i++) .slice(style=`--i: ${i}`)

Moving on to the CSS, we first decide upon a diameter $d for our disc. This is the width of our slices. The height is the diameter divided by the number of items calc(#{$d}/var(--n)).

In order to be able to tell them apart, we give our slices dummy backgrounds determined by parity.

$d: 20em; .slice { --parity: 0; width: $d; height: calc(#{$d}/var(--n)); background: hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%)); &:nth-of-type(2n) { --parity: 1 }
}

We also position our slices in the middle with a column flex layout on their container (the body in our case).

See the Pen by thebabydino (@thebabydino) on CodePen.

To get the disc shape we use a circle() clipping path having the radius $r equal to half the diameter .5*$d and the central point dead in the middle of the assembly. Since we set this clip-path on the slices, the position of the central point for each slice is relative to the slice itself.

Horizontally, it’s always in the middle, at 50% of the slice. Vertically, it needs to be in the middle of the assembly, so that’s where the total number of items and the item’s index which we’ve passed as CSS variables from the preprocessor code come into play.

In the middle of the assembly means at half the height of the assembly from the top of the assembly. Half the height of the assembly is half the diameter .5*$d, which is equivalent to the radius $r. But this value is relative to the whole assembly and we need one that’s relative to the current slice. In order to get this, we subtract the vertical position of the current slice relative to the assembly, that is, how far the top of the current slice is relative to the top of the assembly.

The first slice (of index --i: 0) is at the very top of the assembly, so the amount we subtract in this case is 0.

The second slice (of index --i: 1) is at one slice height from the top of the assembly (the space occupied by the first slice), so the amount we subtract in this case is 1 slice heights.

The third slice (of index --i: 2) is at two slice heights from the top of the assembly (the space occupied by the first and second slices), so the amount we subtract in this case is 2 slice heights.

In the general case, the amount we subtract for each slice is the slice’s index (--i) multiplied by one slice height.

--h: calc(#{d}/var(--n)); /* slice height */
clip-path: circle($r at 50% calc(#{$r} - var(--i)*var(--h))

See the Pen by thebabydino (@thebabydino) on CodePen.

After doing this, we can offset the slices based on parity.

--sign: calc(1 - 2*var(--parity));
transform: translate(calc(var(--sign)*2%))

We now have our sliced disc!

See the Pen by thebabydino (@thebabydino) on CodePen.

Spacing out the slices

The first thought that comes to mind here is to use a margin on each slice.

See the Pen by thebabydino (@thebabydino) on CodePen.

This may be a good result in some cases, but what if we don’t want our disc to get elongated?

Well, we have the option of limiting the background to the content-box and adding a vertical padding:

box-sizing: border-box;
padding: .125em 0;
background: hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%)) content-box;

Of course, in this case, we need to make sure box-sizing is set to border-box so that the vertical padding doesn’t add to the height.

See the Pen by thebabydino (@thebabydino) on CodePen.

The one little problem in this case is that it also cuts off the top of the first slice and the bottom of the last slice. This may not be an issue in some cases and we can always reset the padding-top on the :first-of-type and the padding-bottom on the :last-of-type to 0:

.slice { /* other styles */ padding: .125em 0; &:first-of-type { padding-top: 0 } &:last-of-type { padding-bottom: 0 }
}

However, we also have a one-line solution to this problem of creating gaps in between the slices: add a mask on the container!

This mask is a repeating-linear-gradient() which creates transparent stripes of the thickness of the gap $g, repeats itself after a slice height and is limited to the disc diameter $d horizontally and to the disc diameter $d minus a gap $g vertically (so that we don’t mask out the very top and the very bottom as we also did initially with the padding approach).

mask: repeating-linear-gradient(red 0, red calc(var(--h) - #{$g}), transparent 0, transparent var(--h)) 50% calc(50% - #{.5*$g})/ #{$d} calc(#{$d} - #{$g})

Note that in this case we need to set the slice height variable --h on the container as we’re using it for the mask.

See the Pen by thebabydino (@thebabydino) on CodePen.

Continuous background

In order to have a continuous gradient background, we need to give this background a height equal to that of the disc and set its vertical position relative to each slice such that it always starts from the top of the assembly… wherever that may be located relative to the slice.

The top of the first slice (of index --i: 0) coincides with that of the assembly, so our background starts from 0 vertically.

The top of the second slice (of index --i: 1) is 1 slice height below that of the assembly, so its background starts from 1 slice height above vertically. Since the positive direction of the y axis is down, this means our background-position along the y axis is calc(-1*var(--h)) in this case.

The top of the third slice (of index --i: 2) is 2 slice heights below that of the assembly, so its background starts from 2 slice heights above vertically. This makes our background-position along the y axis is calc(-2*var(--h)).

We notice a pattern here: in general, the background-position along the y axis for a slice is calc(-1*var(--i)*var(--h)).

background: linear-gradient(#eccc05, #c26e4c, #a63959, #4e2255, #333f3d) /* background-position */ 50% calc(-1*var(--i)*var(--h))/ 100% $d /* background-size */

See the Pen by thebabydino (@thebabydino) on CodePen.

But if we want a left to right gradient, then our background isn’t continuous anymore, something that becomes really obvious if we tweak the stop positions a bit in order to have abrupt changes:

background: linear-gradient(90deg, #eccc05 33%, #c26e4c 0, #a63959 67%, #4e2255 0, #333f3d) /* background-position */ 50% calc(-1*var(--i)*var(--h))/ 100% $d /* background-size */

See the Pen by thebabydino (@thebabydino) on CodePen.

In order to fix this issue, we set the offset as a Sass variable $o, set the horizontal background-size to the slice width (100% or $d) plus twice the offset and make sure we attach the background for the slices that move to the left (in the negative direction of the x axis, so by -$o) on the left side of the slice (background-position along the x axis is 0%) and for the slices that move to the right (in the positive direction of the x axis, so by $o) on the right side of the slice (background-position along the x axis is 100%).

$o: 2%;
transform: translate(calc(var(--sign)*#{$o}));
background: linear-gradient(90deg, #eccc05 33%, #c26e4c 0, #a63959 67%, #4e2255 0, #333f3d) /* background-position */ calc((1 - var(--parity))*100%) calc(-1*var(--i)*var(--h))/ calc(100% + #{2*$o}) $d /* background-size */

See the Pen by thebabydino (@thebabydino) on CodePen.

This works for gradients at any angle, as it can be seen in the interactive demo below – drag to change the gradient angle:

See the Pen by thebabydino (@thebabydino) on CodePen.

It also works for images, though in this case we need to remove the second background-size value so the image doesn’t get distorted, which leaves us with the caveat of getting vertical repetition if the image’s aspect ratio is greater than calc(#{$d} + #{2*$o}) : #{$d}. This isn’t the case for the square image we’re using below, but it’s still something to keep in mind.

See the Pen by thebabydino (@thebabydino) on CodePen.

Another thing to note is that above, the top of the image is attached to the top of of the assembly. If we want the middle of the image to be attached to the middle of the assembly, we need to tweak the vertical component of the background-position a bit.

First off, to attach the middle of the image to the middle of a slice, we use a background-position value of 50%. But we don’t want the middle of the image in the middle of each slice, we want it in the middle of the assembly for all slices. We already know the distance from the top of each slice to the vertical midpoint of the whole assembly – it’s the y coordinate of the clipping circle’s central point:

--y: calc(#{$r} - var(--i)*var(--h));
clip-path: circle($r at 50% var(--y))

The distance from the vertical midpoint of each slice to that of the assembly is this value --y minus half a slice’s height. So it results that the background-position we need along the y axis in order to have the vertical midpoint of the image attached to that of the assembly is calc(50% + var(--y) - .5*var(--h)).

See the Pen by thebabydino (@thebabydino) on CodePen.

Incremental slices

This means our slices don’t have the same height anymore. For example, the first one could have a unit height, the second one twice this height, the third one three times this height and so on…

The added heights of all these slices should equal the disc diameter. In other words, we should have the following equality:

h + 2*h + 3*h + ... + n*h = d

This can also be written as:

h*(1 + 2 + 3 + ... + n) = d

which makes it easier to notice something! Within the parenthesis, we have the sum of the first n natural numbers, which is always n*(n + 1)/2!

So our equality becomes:

h*n*(n + 1)/2 = d

This allows us to get the unit height h:

h = 2*d/n/(n + 1)

Applying this to our demo, we have:

--h: calc(#{2*$d}/var(--n)/(var(--n) + 1));
height: calc((var(--i) + 1)*var(--h));

See the Pen by thebabydino (@thebabydino) on CodePen.

Just like in the case of equal slices, the y coordinate of the central point of the clipping circle() is the disc radius $r minus the distance from the top of the assembly to the top of the current slice. This is the sum of the heights of all previous slices.

In the case of the first slice (--i: 0), we have no previous slice, so this sum is 0.

In the case of the second slice (--i: 1), we only have the first slice before and its height is the unit height (--h).

In the case of the third slice (--i: 2), the sum we want is that between the height of the first slice, which equals the unit height and that of the second slice, which is twice the unit height. That’s calc(var(--h) + 2*var(--h)) or calc(var(--h)*(1 + 2)).

In the case of the third slice (--i: 3), the sum is that between the height of the first slice, which equals the unit height, that of the second slice, which is twice the unit height and that of the third slice, which is three times the unit height. That’s calc(var(--h) + 2*var(--h) + 3*var(--h)) or calc(var(--h)*(1 + 2 + 3)).

Now we can see a pattern emerging! For every slice of index --i, we have that the added height of its previous slices is the unit height --h times the sum of the first --i natural numbers (and the sum of the first --i natural numbers is calc(var(--i)*(var(--i) + 1)/2)). This means our clip-path value becomes:

circle($r at 50% calc(var(--h)*var(--i)*(var(--i) + 1)/2))

We add the offset back in and we have the following result:

See the Pen by thebabydino (@thebabydino) on CodePen.

Sadly, having incremental slices means the repeating-linear-gradient() mask method of creating gaps cannot work anymore. What still works however just fine is the vertical padding method and we can set the padding values such that the top one is 0 for the first slice and the bottom one is 0 for the last slice.

padding: calc(var(--i)*#{$g}/var(--n)) /* top */ 0 /* lateral */ calc((var(--n) - 1 - var(--i))*#{$g}/var(--n)) /* bottom */

See the Pen by thebabydino (@thebabydino) on CodePen.

For a gradient background, the main idea remains the same as in the case of the equal slices. There are just two things we need to take into account.

One, the background-position along the y axis is minus the distance (in absolute value) between the top of the assembly and the top of the current slice. This distance isn’t calc(var(--i)*var(--h)) like in the case of equal slices of height --h anymore. Instead it’s, as computed a bit earlier, calc(var(--i)*(var(--i) + 1)/2*var(--h)). So the background-position along the y axis is calc(-1*var(--i)*(var(--i) + 1)/2*var(--h)).

And two, we want our background clipped to the content-box so that we keep the gaps, but we need to keep the background-origin to its initial value of padding-box so that our gradient stays continuous.

background: linear-gradient(var(--a), #eccc05, #c26e4c, #a63959, #4e2255, #333f3d) /* background-position */ calc((1 - var(--parity))*100%) /* x component */ calc(-1*var(--i)*(var(--i) + 1)/2*var(--h)) /* y component */ / /* background-size */ calc(100% + #{2*$o}) $d padding-box /* background-origin */ content-box /* background-clip */;

See the Pen by thebabydino (@thebabydino) on CodePen.

For an image background whose midpoint is attached to the middle of our assembly, we need to take into account the fact that half a slice height isn’t the same value for all slices anymore. Now the height of a slice is calc((var(--i) + 1)*var(--h)), so this is the value we need to subtract in the formula for the y component of the background-position.

--y: calc(#{$r} - .5*var(--i)*(var(--i) + 1)*var(--h));
background: url(/amur_leopard.jpg) /* background-position */ calc((1 - var(--parity))*100%) /* x component */ calc(50% + var(--y) - .5*(var(--i) + 1)*var(--h)) /* y component */ / /* background-size */ calc(100% + #{2*$o}) padding-box /* background-origin */ content-box /* background-clip */;
clip-path: circle($r at 50% var(--y));

See the Pen by thebabydino (@thebabydino) on CodePen.

Vertical slices

We can also slice our disc along the other direction. This means removing the flex-direction: column declaration from the container and letting the flex-direction be the initial one (row), switching the width and the height, the x and y coordinates of the circular clipping path’s central point, the direction along which we shift the slices, the dimensions and x and y positions of the masking gradient, which we also need to rotate so that it goes along the x axis.

body { /* same as before */ --w: calc(#{$d}/var(--n)); mask: repeating-linear-gradient(90deg, red 0, red calc(var(--w) - #{$g}), transparent 0, transparent var(--w)) calc(50% - #{.5*$g}) 50% / calc(#{$d} - #{$g}) #{$d}
} .slice { /* same as before */ width: var(--w); height: $d; transform: translatey(calc(var(--sign)*2%)); background: hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%)); clip-path: circle($r at calc(#{$r} - var(--i)*var(--w)) 50%)
}

This gives us equal vertical slices with alternating backgrounds:

See the Pen by thebabydino (@thebabydino) on CodePen.

For the gradient case, we need to also reverse the two background dimensions and the background positions along the x and y axes:

background: linear-gradient(135deg, #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%) /* background-position */ calc(-1*var(--i)*var(--w)) calc((1 - var(--parity))*100%)/ #{$d} calc(100% + #{2*$o}) /* background-size */

See the Pen by thebabydino (@thebabydino) on CodePen.

For incremental slices, we combine the incremental case with the vertical case, which means swapping the values we have for the previous incremental case along the two axes:

--w: calc(#{2*$d}/var(--n)/(var(--n) + 1));
width: calc((var(--i) + 1)*var(--w)); height: $d;
clip-path: circle($r at calc(#{$r} - .5*var(--i)*(var(--i) + 1)*var(--w)) 50%);

See the Pen by thebabydino (@thebabydino) on CodePen.

To create the gaps, we use the padding method. But since we’re now in the vertical case, we need horizontal paddings, on the left and on the right and to make sure the padding-left for the first slice is 0 and the padding-right for the last slice is also 0:

box-sizing: border-box;
padding: 0 /* top */ calc((var(--n) - 1 - var(--i))*#{$g}/var(--n)) /* right */ 0 /* bottom */ calc(var(--i)*#{$g}/var(--n)) /* left */;
background: hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%)) content-box

See the Pen by thebabydino (@thebabydino) on CodePen.

Finally, we have the gradient case:

background: linear-gradient(135deg, #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%) /* background-position */ calc(-.5*var(--i)*(var(--i) + 1)*var(--w)) /* x component */ calc((1 - var(--parity))*100%) /* y component */ / /* background-size */ #{$d} calc(100% + #{2*$o}) padding-box /* background-origin */ content-box /* background-clip */;

See the Pen by thebabydino (@thebabydino) on CodePen.

2D case

Again, we generate it with a bit of Pug, the total number of items being the product between the number of columns and the number of rows. For simplicity, we keep the number of rows and the number of columns equal.

- var n = 8, m = Math.pow(n, 2); style :root { --n: #{n}; --i: 0; --j: 0 } - for(var i = 1; i < n; i++) { | .tile:nth-of-type(#{n}n + #{i + 1}) { --i: #{i} } | .tile:nth-of-type(n + #{n*i + 1}) { --j: #{i} } - }
- for(var i = 0; i < m; i++) .tile

We’ve also passed the column and row indices (--i and --j respectively) to the CSS.

Since we’re in the 2D case, we switch from using a 1D layout (flex) to using a 2D one (grid). We also start with the disc diameter $d and, given the number of columns is equal to that of rows (--n), our disc gets divided into identical tiles of edge length --l: calc(#{$d}/var(--n)).

$d: 20em; body { --l: calc(#{$d}/var(--n)); display: grid; place-content: center; grid-template: repeat(var(--n), var(--l))/ repeat(var(--n), var(--l))
}

To create the gaps in between the tiles, we use the padding approach on the .tile elements and combine the horizontal and vertical cases such that we have the padding-top for the first row is 0, the padding-left for the first column is 0, the padding-bottom for the last row is 0 and the padding-right for the last-column is 0.

padding: calc(var(--j)*#{$g}/var(--n)) /* top */ calc((var(--n) - 1 - var(--i))*#{$g}/var(--n)) /* right */ calc((var(--n) - 1 - var(--j))*#{$g}/var(--n)) /* bottom */ calc(var(--i)*#{$g}/var(--n)) /* left */

Note that we’ve used the row index --j for the top to bottom direction (vertical paddings) and the column index --i from the left to right direction (lateral paddings).

To get the disc shape, we again combine the horizontal and vertical cases, using the column index --i to get the x coordinate of the circular clipping path’s central point and the row index --j to get its y coordinate.

clip-path: circle($r at calc(#{$r} - var(--i)*var(--l)) calc(#{$r} - var(--j)*var(--l)))

See the Pen by thebabydino (@thebabydino) on CodePen.

For a gradient background, it’s again combining the horizontal and the vertical cases and taking into account that here we have no offset at this point, which means the background-size is the disc diameter $d along both axes.

background: linear-gradient(135deg, #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%) /* background-position */ calc(-1*var(--i)*var(--l)) calc(-1*var(--j)*var(--l)) / #{$d} #{$d} /* background-size */ padding-box /* background-origin */ content-box /* background-clip */

See the Pen by thebabydino (@thebabydino) on CodePen.

For an image background, we remove the second background-size value so we prevent the image from getting stretched if it’s not square. We also adapt the code for attaching the image’s midpoint to that of the grid from the 1D case to the 2D case:

--x: calc(#{$r} - var(--i)*var(--l));
--y: calc(#{$r} - var(--j)*var(--l));
background: url(/amur_leopard.jpg) /* background-position */ calc(50% + var(--x) - .5*var(--l)) calc(50% + var(--y) - .5*var(--l)) / #{$d} /* background-size */ padding-box /* background-origin */ content-box /* background-clip */;
clip-path: circle($r at var(--x) var(--y))

See the Pen by thebabydino (@thebabydino) on CodePen.

In the incremental case, we don’t have the same dimensions for all tiles, so we use auto sizing for grid-template:

body { /* same as before */ grid-template: repeat(var(--n), auto)/ repeat(var(--n), auto)
}

Just like in the 1D case, we start by computing a unit edge length --u:

--u: calc(#{2*$d}/var(--n)/(var(--n) + 1))

We then set incremental dimensions along both axes for our tile elements:

width: calc((var(--i) + 1)*var(--u));
height: calc((var(--j) + 1)*var(--u))

We also need to adapt the coordinates of the clipping circle’s central point to the incremental case:

clip-path: circle($r at calc(#{$r} - .5*var(--i)*(var(--i) + 1)*var(--u)) calc(#{$r} - .5*var(--j)*(var(--j) + 1)*var(--u)))

See the Pen by thebabydino (@thebabydino) on CodePen.

For a gradient background, we adapt the equal tiles version to the incremental case. This means tweaking the background-position as we did before for the incremental slices, only this time we do it along both axes, not just along one:

background: linear-gradient(135deg, #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%) /* background-position */ calc(-.5*var(--i)*(var(--i) + 1)*var(--l)) calc(-.5*var(--j)*(var(--j) + 1)*var(--l)) / #{$d} #{$d} /* background-size */ padding-box /* background-origin */ content-box /* background-clip */

See the Pen by thebabydino (@thebabydino) on CodePen.

Finally, we have the image background option for the incremental 2D case:

background: url(/amur_leopard.jpg) /* background-position */ calc(50% + var(--x) - .5*(var(--i) + 1)*var(--u)) calc(50% + var(--y) - .5*(var(--j) + 1)*var(--u)) / #{$d} /* background-size */ padding-box /* background-origin */ content-box /* background-clip */

See the Pen by thebabydino (@thebabydino) on CodePen.

There are probably more variations we could be coming up with, but we’ll stop here. If you have more ideas on how to push this further, I’d love to hear about them!

The post Slice and Dice a Disc with CSS appeared first on CSS-Tricks.

An Initial Implementation of clip-path: path();

One thing that has long surprised (and saddened) me is that the clip-path property, as awesome as it is, only takes a few values. The circle() and ellipse() functions are nice, but hiding overflows and rounding with border-radius generally helps there already. Perhaps the most useful value is polygon() because it allows us to draw a shape out of straight lines at arbitrary points.

Here’s a demo of each value:

See the Pen clip-path examples by Chris Coyier (@chriscoyier) on CodePen.

The sad part comes in when you find out that clip-path doesn’t accept path(). C’mon it’s got path in the name! The path syntax, which comes from SVG, is the ultimate syntax. It allows us to draw literally any shape.

More confusingly, there already is a path() function, which is what properties like offset-path take.

I was once so flabbergasted by all this that I turned it into a full conference talk.

The talk goes into the shape-outside property and how it can’t use path(). It also goes into the fact that we can change the d property of a literal <path>.

I don’t really blame anyone, though. This is weird stuff and it’s being implemented by different teams, which inevitably results in different outcomes. Even the fact that SVG uses unit-less values in the <path> syntax is a little weird and an anomaly in CSS-land. How that behaves, how values with units behave, what comma-syntax is allowed and disallowed, and what the DOM returns when asked is plenty to make your head spin.

Anyway! Along comes Firefox with an implementation!

Here’s that flag in Firefox (layout.css.clip-path-path.enabled):

And here’s a demo… you’ll see a square in unsupported browsers and a heart in the ones that support clip-path: path(); — which is only Firefox Nightly with the flag turned on at the time of this writing.

See the Pen clip-path: path()! by Chris Coyier (@chriscoyier) on CodePen.

A screenshot of clip-path: path() working in Firefox Nightly

Now, all we need is:

  • clip-path to be able to point to the URL of a <clipPath> in SVG, like url("#clip-path");
  • shape-outside to be able to use path()
  • shape-outside to be able to use a <clipPath>
  • offset-path to take all the other shape functions
  • Probably a bunch of specs to make sure this is all handled cleanly (Good luck, team!)
  • Browsers to implement it all

😉

The post An Initial Implementation of clip-path: path(); appeared first on CSS-Tricks.

People Talkin’ Shapes

Codrops has a very nice article on CSS Shapes from Tania Rascia. You might know shape-outside is for redefining the area by which text is floated around that element, allowing for some interesting design opportunities. But there are a couple of genuine CSS tricks in here:

  1. Float shape-outside elements both right and left to get text to flow between them.
  2. You can set shape-outside to take an image and use shape-image-threshold to adjust where the text flows, meaning you could even use a gradient!


Shapes are in the water recently, as Heydon Pickering recently published a short video on using them. He also covers things like clip-path and canvas and such:


We recently moved our long-time page on (basically faking) CSS shapes over to a blog post so it’s easier to maintain.

Robin also wrote Working with Shapes in Web Design that digs into all this. So many tricks!

See the Pen 10c03204463e92a72a6756678e6348d1 by CSS-Tricks (@css-tricks) on CodePen.


When we talk about CSS shapes, it’s almost like we’re talking about values moreso than properties. What I mean is that the value functions like polygon(), circle(), ellipse(), offset(), path(), etc. are more representative of “CSS shapes” than the properties they are applied to. Multiple properties take them, like shape-outside, clip-path, and offset-path.

I once did a whole talk on this:

The only thing that’s changed since then is that Firefox started allowing clip-path: path() behind the flag layout.css.clip-path-path.enabled (demo).


And don’t forget Jen Simmons was talking about the possibilities of CSS Shapes (in her lab demos) years earlier!

The post People Talkin’ Shapes appeared first on CSS-Tricks.

Using CSS Clip Path to Create Interactive Effects, Part II

This is a follow up to my previous post looking into clip paths. Last time around, we dug into the fundamentals of clipping and how to get started. We looked at some ideas to exemplify what we can do with clipping. We’re going to take things a step further in this post and look at different examples, discuss alternative techniques, and consider how to approach our work to be cross-browser compatible.

One of the biggest drawbacks of CSS clipping, at the time of writing, is browser support. Not having 100% browser coverage means different experiences for viewers in different browsers. We, as developers, can’t control what browsers support — browser vendors are the ones who implement the spec and different vendors will have different agendas.

One thing we can do to overcome inconsistencies is use alternative technologies. The feature set of CSS and SVG sometimes overlap. What works in one may work in the other and vice versa. As it happens, the concept of clipping exists in both CSS and SVG. The SVG clipping syntax is quite different, but it works the same. The good thing about SVG clipping compared to CSS is its maturity level. Support is good all the way back to old IE browsers. Most bugs are fixed by now (or at least one hope they are).

This is what the SVG clipping support looks like:

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 Opera Firefox IE Edge Safari
4 9 3 9 12 3.2

Mobile / Tablet

iOS Safari Opera Mobile Opera Mini Android Android Chrome Android Firefox
3.2 10 all 4.4 67 60

Clipping as a transition

A neat use case for clipping is transition effects. Take The Silhouette Slideshow demo on CodePen:

See the Pen Silhouette zoom slideshow by Mikael Ainalem (@ainalem) on CodePen.

A “regular” slideshow cycles though images. Here, to make it a bit more interesting, there’s a clipping effect when switching images. The next image enters the screen through a silhouette of of the previous image. This creates the illusion that the images are connected to one another, even if they are not.

The transitions follow this process:

  1. Identify the focal point (i.e., main subject) of the image
  2. Create a clipping path for that object
  3. Cut the next image with the path
  4. The cut image (silhouette) fades in
  5. Scale the clipping path until it’s bigger than the viewport
  6. Complete the transition to display the next image
  7. Repeat!

Let’s break down the sequence, starting with the first image. We’ll split this up into multiple pens so we can isolate each step.

<svg> ... <image class="..." xlink:href="..." /> ... </svg>

For this image, we then want to create a mask of the focal point — in this case, the person’s silhouette. If you’re unsure how to go about creating a clip, check out my previous article for more details because, generally speaking, making cuts in CSS and SVG is fundamentally the same:

  1. Import an image into the SVG editor
  2. Draw a path around the object
  3. Convert the path to the syntax for SVG clip path. This is what goes in the SVG’s <defs> block.
  4. Paste the SVG markup into the HTML

If you’re handy with the editor, you can do most of the above in the editor. Most editors have good support for masks and clip paths. I like to have more control over the markup, so I usually do at least some of the work by hand. I find there’s a balance between working with an SVG editor vs. working with markup. For example, I like to organize the code, rename the classes and clean up any cruft the editor may have dropped in there.

Mozilla Developer Network does a fine job of documenting SVG clip paths. Here’s a stripped-down version of the markup used by the original demo to give you an idea of how a clip path fits in:

<svg> <defs> <clipPath id="clip"> <!-- Clipping defined --> <path class="clipPath clipPath2" d="..." /> </clipPath> </defs> ... <path ... clip-path="url(#clip)"/> <!-- Clipping applied -->
</svg>

Let’s use a colored rectangle as a placeholder for the next image in the slideshow. This helps to clearly visualize the shape that part that’s cut out and will give a clearer idea of the shape and its movement.

.clipPath { transition: transform 1200ms 500ms; /* Delayed transform transition */ transform-origin: 50%; } .clipPath.active { transform: translateX(-30%) scale(15); /* Upscaling and centering mask */ } .image { transition: opacity 1000ms; /* Fade-in, starts immediately */ opacity: 0; } .image.active { opacity: 1; }

Here’s what we get — an image that transitions to the rectangle!

remove = (remove + 1) % images.length; current = (current + 1) % images.length

Note that this examples is not supported by Firefox at the time of writing because is lacks support for scaling clip paths. I hope this is something that will be addressed in the near future.

Clipping to emerge foreground objects into the background

Another interesting use for clipping is for revealing and hiding effects. We can create parts of the view where objects are either partly or completely hidden making for a fun way to make background images interact with foreground content. For instance, we could have objects disappear behind elements in the background image, say a building or a mountain. It becomes even more interesting when we pair that idea up with animation or scrolling effects.

See the Pen Parallax clip by Mikael Ainalem (@ainalem) on CodePen.

This example uses a clipping path to create an effect where text submerges into the photo — specifically, floating behind mountains as a user scrolls down the page. To make it even more interesting, the text moves with a parallax effect. In other words, the different layers move at different speeds to enhance the perspective.

We start with a simple div and define a background image for it in the CSS:

window.addEventListener('scroll', function() { logo.setAttribute('transform',`translate(0 ${html.scrollTop / 10 + 5})`); clip.setAttribute('transform',`translate(0 -${html.scrollTop / 10 + 5})`); });

Don’t pay too much attention to the + 5 used when calculating the distance. It’s only there as a sloppy way to offset the element. The important part is where things are divided by 10, which creates the parallax effect. Scrolling a certain amount will proportionally move the element and the clip path. Template literals convert the calculated value to a string which is used for the transform property value as an offset to the SVG nodes.

Combining clipping and masking

Clipping and masking are two interesting concepts. One lets you cut out pieces of content whereas the other let’s you do the opposite. Both techniques are useful by themselves but there is no reason why we can’t combine their powers!

When combining clipping and masking, you can split up objects to create different visual effects on different parts. For example:

See the Pen parallax logo blend by Mikael Ainalem (@ainalem) on CodePen.

I created this effect using both clipping and masking on a logo. The text, split into two parts, blends with the background image, which is a beautiful monochromatic image of the New York’s Statue of Liberty. I use different colors and opacities on different parts of the text to make it stand out. This creates an interesting visual effect where the text blends in with the background when it overlaps with the statue — a splash of color to an otherwise grey image. There is, besides clipping and masking, a parallax effect here as well. The text moves in a different speed relative to the image when the user hovers or moves (touch) over the image.

To illustrate the behavior, here is what we get when the masked part is stripped out:

See the Pen parallax logo blend by Mikael Ainalem (@ainalem) on CodePen.

Wrapping up

Clipping is a fun way to create interactions and visual effects. It can enhance slide-shows or make objects stand out of images, among other things. Both SVG and CSS provide the ability to apply clip paths and masks to elements, though with different syntaxes. We can pretty much cut any web content nowadays. It is only your imagination that sets the limit.

If you happen to create anything cool with the things we covered here, please share them with me in the comments!

The post Using CSS Clip Path to Create Interactive Effects, Part II appeared first on CSS-Tricks.