Animating with Clip-Path

clip-path is one of those CSS properties we generally know is there but might not reach for often for whatever reason. It’s a little intimidating in the sense that it feels like math class because it requires working with geometric shapes, each with different values that draw certain shapes in certain ways.

We’re going to dive right into clip-path in this article, specifically looking at how we can use it to create pretty complex animations. I hope you’ll see just how awesome the property and it’s shape-shifting powers can be.

But first, let’s do a quick recap of what we’re working with.

Clip-path crash course

Just for a quick explanation as to what the clip-path is and what it provides, MDN describes it like this:

The clip-path CSS property creates a clipping region that sets what part of an element should be shown. Parts that are inside the region are shown, while those outside are hidden.

Consider the circle shape provided by clip-path. Once the circle is defined, the area inside it can be considered "positive" and the area outside it "negative." The positive space is rendered while the negative space is removed. Taking advantage of the fact that this relationship between positive and negative space can be animated provides for interesting transition effects… which is what we’re getting into in just a bit.

clip-path comes with four shapes out of the box, plus the ability to use a URL to provide a source to some other SVG <clipPath> element. I’ll let the CSS-Tricks almanac go into deeper detail, but here are examples of those first four shapes.

Shape Example Result
Circle clip-path: circle(25% at 25% 25%);
Ellipse clip-path: ellipse(25% 50% at 25% 50%);
Inset clip-path: inset(10% 20% 30% 40% round 25%);
Polygon clip-path: polygon(50% 25%, 75% 75%, 25% 75%);

Combining clippings with CSS transitions

Animating clip-path can be as simple as changing the property values from one shape to another using CSS transitions, triggered either by changing classes in JavaScript or an interactive change in state, like :hover:

.box {
  clip-path: circle(75%);
  transition: clip-path 1s;
}

.box:hover {
  clip-path: circle(25%);
}

See the Pen
clip-path with transition
by Geoff Graham (@geoffgraham)
on CodePen.

We can also use CSS animations:

@keyframes circle {
  0% { clip-path: circle(75%); }
  100% { clip-path: circle(25%); }
}

See the Pen
clip-path with CSS animation
by Geoff Graham (@geoffgraham)
on CodePen.

Some things to consider when animating clip-path:

  • It only affects what is rendered and does not change the box size of the element as relating to elements around it. For example, a floated box with text flowing around it will still take up the same amount of space even with a very small percentage clip-path applied to it.
  • Any CSS properties that extend beyond the box size of the element may be clipped. For example, an inset of 0% for all four sides that clips at the edges of the element will remove a box-shadow and require a negative percentage to see the box-shadow. Although, that could lead to interesting effects in of itself!

OK, let’s get into some light animation to kick things off.

Comparing the simple shapes

I’ve put together a demo where you can see each shape in action, along with a little explanation describing what’s happening.

See the Pen
Animating Clip-Path: Simple Shapes
by Travis Almand (@talmand)
on CodePen.

The demo makes use of Vue for the functionality, but the CSS is easily transferred to any other type of project.

We can break those out a little more to get a handle on the values for each shape and how changing them affects the movement

Circle

clip-path: circle(<length|percentage> at <position>);

Circle accepts two properties that can be animated:

  1. Shape radius: can be a length or percentage
  2. Position: can be a length or percentage along the x and y axis
.circle-enter-active { animation: 1s circle reverse; }
.circle-leave-active { animation: 1s circle; }

@keyframes circle {
  0% { clip-path: circle(75%); }
  100% { clip-path: circle(0%); }
}

The circle shape is resized in the leave transition from an initial 75% radius (just enough to allow the element to appear fully) down to 0%. Since no position is set, the circle defaults to the center of the element both vertically and horizontally. The enter transition plays the animation in reverse by means of the "reverse" keyword in the animation property.

Ellipse

clip-path: ellipse(<length|percentage>{2} at <position>);

Ellipse accepts three properties that can be animated:

  1. Shape radius: can be a length or percentage on the horizontal axis
  2. Shape radius: can be a length or percentage on the vertical axis
  3. Position: can be a length or percentage along the x and y axis
.ellipse-enter-active { animation: 1s ellipse reverse; }
.ellipse-leave-active { animation: 1s ellipse; }

@keyframes ellipse {
  0% { clip-path: ellipse(80% 80%); }
  100% { clip-path: ellipse(0% 20%); }
}

The ellipse shape is resized in the leave transition from an initial 80% by 80%, which makes it a circular shape larger than the box, down to 0% by 20%. Since no position is set, the ellipse defaults to the center of the box both vertically and horizontally. The enter transition plays the animation in reverse by means of the "reverse" keyword in the animation property.

The effect is a shrinking circle that changes to a shrinking ellipse taller than wide wiping away the first element. Then the elements switch with the second element appearing inside the growing ellipse.

Inset

clip-path: inset(<length|percentage>{1,4} round <border-radius>{1,4});

The inset shape has up to five properties that can be animated. The first four represent each edge of the shape and behave similar to margins or padding. The first property is required while the next three are optional depending on the desired shape.

  1. Length/Percentage: can represent all four sides, top/bottom sides, or top side
  2. Length/Percentage: can represent left/right sides or right side
  3. Length/Percentage: represents the bottom side
  4. Length/Percentage: represents the left side
  5. Border radius: requires the "round" keyword before the value

One thing to keep in mind is that the values used are reversed from typical CSS usage. Defining an edge with zero means that nothing has changed, the shape is pushed outward to the element’s side. As the number is increased, say to 10%, the edge of the shape is pushed inward away from the element’s side.

.inset-enter-active { animation: 1s inset reverse; }
.inset-leave-active { animation: 1s inset; }

@keyframes inset {
  0% { clip-path: inset(0% round 0%); }
  100% { clip-path: inset(50% round 50%); }
}

The inset shape is resized in the leave transition from a full-sized square down to a circle because of the rounded corners changing from 0% to 50%. Without the round value, it would appear as a shrinking square. The enter transition plays the animation in reverse by means of the "reverse" keyword in the animation property.

The effect is a shrinking square that shifts to a shrinking circle wiping away the first element. After the elements switch the second element appears within the growing circle that shifts to a growing square.

Polygon

clip-path: polygon(<length|percentage>);

The polygon shape is a somewhat special case in terms of the properties it can animate. Each property represents vertices of the shape and at least three is required. The number of vertices beyond the required three is only limited by the requirements of the desired shape. For each keyframe of an animation, or the two steps in a transition, the number of vertices must always match for a smooth animation. A change in the number of vertices can be animated, but will cause a popping in or out effect at each keyframe.

.polygon-enter-active { animation: 1s polygon reverse; }
.polygon-leave-active { animation: 1s polygon; }

@keyframes polygon {
  0% { clip-path: polygon(0 0, 50% 0, 100% 0, 100% 50%, 100% 100%, 50% 100%, 0 100%, 0 50%); }
  100% { clip-path:  polygon(50% 50%, 50% 25%, 50% 50%, 75% 50%, 50% 50%, 50% 75%, 50% 50%, 25% 50%); }
}

The eight vertices in the polygon shape make a square with a vertex in the four corners and the midpoint of all four sides. On the leave transition, the shape’s corners animate inwards to the center while the side’s midpoints animate inward halfway to the center. The enter transition plays the animation in reverse by means of the "reverse" keyword in the animation property.

The effect is a square that collapses inward down to a plus shape that wipes away the element. The elements then switch with the second element appears in a growing plus shape that expands into a square.

Let’s get into some simple movements

OK, we’re going to dial things up a bit now that we’ve gotten past the basics. This demo shows various ways to have movement in a clip-path animation. The circle and ellipse shapes provide an easy way to animate movement through the position of the shape. The inset and polygon shapes can be animated in a way to give the appearance of position-based movement.

See the Pen
Animating Clip-Path: Simple Movements
by Travis Almand (@talmand)
on CodePen.

Let’s break those out just like we did before.

Slide Down

The slide down transition consists of two different animations using the inset shape. The first, which is the leave animation, animates the top value of the inset shape from 0% to 100% providing the appearance of the entire square sliding downward out of view. The second, which is the enter animation, has the bottom value at 100% and then animates it down towards 0% providing the appearance of the entire square sliding downward into view.

.down-enter-active { animation: 1s down-enter; }
.down-leave-active { animation: 1s down-leave; }

@keyframes down-enter {
  0% { clip-path: inset(0 0 100% 0); }
  100% { clip-path: inset(0); }
}

@keyframes down-leave {
  0% { clip-path: inset(0); }
  100% { clip-path: inset(100% 0 0 0); }
}

As you can see, the number of sides being defined in the inset path do not need to match. When the shape needs to be the full square, a single zero is all that is required. It can then animate to the new state even when the number of defined sides increases to four.

Box-Wipe

The box-wipe transition consists of two animations, again using the inset shape. The first, which is the leave animation, animates the entire square down to a half-size squared positioned on the element’s left side. The smaller square then slides to the right out of view. The second, which is the enter animation, animates a similar half-size square into view from the left over to the element’s right side. Then it expands outward to reveal the entire element.

.box-wipe-enter-active { animation: 1s box-wipe-enter; }
.box-wipe-leave-active { animation: 1s box-wipe-leave; }

@keyframes box-wipe-enter {
  0% { clip-path: inset(25% 100% 25% -50%); }
  50% { clip-path: inset(25% 0% 25% 50%); }
  100% { clip-path: inset(0); }
}

@keyframes box-wipe-leave {
  0% { clip-path: inset(0); }
  50% { clip-path: inset(25% 50% 25% 0%); }
  100% { clip-path: inset(25% -50% 25% 100%); }
}

When the full element is shown, the inset is at zero. The 50% keyframes define a half-size square that is placed on either the left or right. There are two values representing the left and right edges are swapped. Then the square is then moved to the opposite side. As one side is pushed to 100%, the other must go to -50% to maintain the shape. If it were to animate to zero instead of -50%, then the square would shrink as it animated across instead of moving out of view.

Rotate

The rotate transition is one animation with five keyframes using the polygon shape. The initial keyframe defines the polygon with four vertices that shows the entire element. Then, the next keyframe changes the x and y coordinates of each vertex to be moved inward and near the next vertex in a clockwise fashion. After all four vertices have been transitioned, it appears the square has shrunk and rotated a quarter turn. The following keyframes do the same until the square is collapsed down to the center of the element. The leave transition plays the animation normally while the enter transition plays the animation in reverse.

.rotate-enter-active { animation: 1s rotate reverse; }
.rotate-leave-active { animation: 1s rotate; }

@keyframes rotate {
  0% { clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%); }
  25% { clip-path: polygon(87.5% 12.5%, 87.5% 87.5%, 12.5% 87.5%, 12.5% 12.5%); }
  50% { clip-path: polygon(75% 75%, 25% 75%, 25% 25%, 75% 25%); }
  75% { clip-path: polygon(37.5% 62.5%, 37.5% 37.5%, 62.5% 37.5%, 62.5% 62.5%); }
  100% { clip-path: polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%); }
}

Polygons can be animated into any other position once its vertices have been set, as long as each keyframe has the same number of vertices. This can make many interesting effects with careful planning.

Spotlight

The spotlight transition is one animation with five keyframes using the circle shape. The initial keyframe defines a full-size circle positioned at the center to show the entire element. The next keyframe shrinks the circle down to twenty percent. Each following keyframe animates the position values of the circle to move it to different points on the element until it moves out of view to the left. The leave transition plays the animation normally while the enter transition plays the animation in reverse.

.spotlight-enter-active { animation: 2s spotlight reverse; }
.spotlight-leave-active { animation: 2s spotlight; }

@keyframes spotlight {
  0% { clip-path: circle(100% at 50% 50%); }
  25% { clip-path: circle(20% at 50% 50%); }
  50% { clip-path: circle(20% at 12% 84%); }
  75% { clip-path: circle(20% at 93% 51%); }
  100% { clip-path: circle(20% at -30% 20%); }
}

This may be a complex-looking animation at first, but turns out it only requires simple changes in each keyframe.

More adventurous stuff

Like the shapes and simple movements examples, I’ve made a demo that contains more complex animations. We’ll break these down individually as well.

See the Pen
Animating Clip-Path: Complex Shapes
by Travis Almand (@talmand)
on CodePen.

All of these examples make heavy use of the polygon shape. They take advantage of features like stacking vertices to make elements appear "welded" and repositioning vertices around for movement.

Check out Ana Tudor’s "Cutting out the inner part of an element using clip-path" article for a more in-depth example that uses the polygon shape to create complex shapes.

Chevron

The chevron transition is made of two animations, each with three keyframes. The leave transition starts out as a full square with six vertices; there are the four corners but there are an additional two vertices on the left and right sides. The second keyframe animates three of the vertices into place to change the square into a chevron. The third keyframe then moves the vertices out of view to the right. After the elements switch, the enter transition starts with the same chevron shape but it is out of view on the left. The second keyframe moves the chevron into view and then the third keyframe restores the full square.

.chevron-enter-active { animation: 1s chevron-enter; }
.chevron-leave-active { animation: 1s chevron-leave; }

@keyframes chevron-enter {
  0% { clip-path: polygon(-25% 0%, 0% 50%, -25% 100%, -100% 100%, -75% 50%, -100% 0%); }
  75% { clip-path: polygon(75% 0%, 100% 50%, 75% 100%, 0% 100%, 25% 50%, 0% 0%); }
  100% { clip-path: polygon(100% 0%, 100% 50%, 100% 100%, 0% 100%, 0% 50%, 0% 0%); }
}

@keyframes chevron-leave {
  0% { clip-path: polygon(100% 0%, 100% 50%, 100% 100%, 0% 100%, 0% 50%, 0% 0%); }
  25% { clip-path: polygon(75% 0%, 100% 50%, 75% 100%, 0% 100%, 25% 50%, 0% 0%); }
  100% { clip-path: polygon(175% 0%, 200% 50%, 175% 100%, 100% 100%, 125% 50%, 100% 0%) }
}

Spiral

The spiral transition is a strong example of a complicated series of vertices in the polygon shape. The polygon is created to define a shape that spirals inward clockwise from the upper-left of the element. Since the vertices create lines that stack on top of each other, it all appears as a single square. Over the eight keyframes of the animation, vertices are moved to be on top of neighboring vertices. This makes the shape appear to unwind counter-clockwise to the upper-left, wiping away the element during the leave transition. The enter transition replays the animation in reverse.

.spiral-enter-active { animation: 1s spiral reverse; }
.spiral-leave-active { animation: 1s spiral; }

@keyframes spiral {  
  0% { clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 25%, 75% 25%, 75% 75%, 25% 75%, 25% 50%, 50% 50%, 25% 50%, 25% 75%, 75% 75%, 75% 25%, 0% 25%); }
  14.25% { clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 25%, 75% 25%, 75% 75%, 50% 75%, 50% 50%, 50% 50%, 25% 50%, 25% 75%, 75% 75%, 75% 25%, 0% 25%); }
  28.5% { clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 25%, 75% 25%, 75% 50%, 50% 50%, 50% 50%, 50% 50%, 25% 50%, 25% 75%, 75% 75%, 75% 25%, 0% 25%); }
  42.75% { clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 25%, 25% 25%, 25% 50%, 25% 50%, 25% 50%, 25% 50%, 25% 50%, 25% 75%, 75% 75%, 75% 25%, 0% 25%); }
  57% { clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 75%, 25% 75%, 25% 75%, 25% 75%, 25% 75%, 25% 75%, 25% 75%, 25% 75%, 75% 75%, 75% 25%, 0% 25%); }
  71.25% { clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 75% 100%, 75% 75%, 75% 75%, 75% 75%, 75% 75%, 75% 75%, 75% 75%, 75% 75%, 75% 75%, 75% 75%, 75% 25%, 0% 25%); }
  85.5% { clip-path: polygon(0% 0%, 100% 0%, 100% 25%, 75% 25%, 75% 25%, 75% 25%, 75% 25%, 75% 25%, 75% 25%, 75% 25%, 75% 25%, 75% 25%, 75% 25%, 75% 25%, 0% 25%); }
  100% {clip-path: polygon(0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 25%, 0% 25%, 0% 25%, 0% 25%, 0% 25%, 0% 25%, 0% 25%); }
}

Slots

The slots transition is made of a series of vertices arranged in a pattern of vertical slots with vertices stacked on top of each other for a complete square. The general idea is that the shape starts in the upper-left and the next vertex is 14% to the right. Next vertex is in the exact same spot. Then the one after that is another 14% to the right, and so on until the upper-right corner is reached. This creates a series of "sections" along the top of the shape that are aligned horizontally. The second keyframe then animates every even section downward to the bottom of the element. This gives the appearance of vertical slots wiping away their parts of the element. The third keyframe then moves the remaining sections at the top to the bottom. Overall, the leave transition wipes away half the element in vertical slots and then the other half. The enter transition reverses the animation.

.slots-enter-active { animation: 1s slots reverse; }
.slots-leave-active { animation: 1s slots; }

@keyframes slots {
  0% {
    clip-path: polygon(0% 0%, 14% 0%, 14% 0%, 28% 0%, 28% 0%, 42% 0%, 42% 0%, 56% 0%, 56% 0%, 70% 0%, 70% 0%, 84% 0%, 84% 0%, 100% 0, 100% 100%, 0% 100%);
  }
  50% {
    clip-path: polygon(0% 0%, 14% 0%, 14% 100%, 28% 100%, 28% 0%, 42% 0%, 42% 100%, 56% 100%, 56% 0%, 70% 0%, 70% 100%, 84% 100%, 84% 0%, 100% 0, 100% 100%, 0% 100%);
  }
  100% {
    clip-path: polygon(0% 100%, 14% 100%, 14% 100%, 28% 100%, 28% 100%, 42% 100%, 42% 100%, 56% 100%, 56% 100%, 70% 100%, 70% 100%, 84% 100%, 84% 100%, 100% 100%, 100% 100%, 0% 100%);
  }
}

Shutters

The shutters transition is very similar to the slots transition above. Instead of sections along the top, it creates vertical sections that are placed in line with each other to create the entire square. Starting with the upper-left the second vertex is positioned at the top and 20% to the right. The next vertex is placed in the same place horizontally but is at the bottom of the element. The next vertex after that is in the same spot with the next one back at the top on top of the vertex from two steps ago. This is repeated several times across the element until the right side is reached. If the lines of the shape were visible, then it would appear as a series of vertical sections lined up horizontally across the element. During the animation the left side of each section is moved over to be on top of the right side. This creates a wiping effect that looks like vertical shutters of a window. The enter transition plays the animation in reverse.

.shutters-enter-active { animation: 1s shutters reverse; }
.shutters-leave-active { animation: 1s shutters; }

@keyframes shutters {
  0% {
    clip-path: polygon(0% 0%, 20% 0%, 20% 100%, 20% 100%, 20% 0%, 40% 0%, 40% 100%, 40% 100%, 40% 0%, 60% 0%, 60% 100%, 60% 100%, 60% 0%, 80% 0%, 80% 100%, 80% 100%, 80% 0%, 100% 0%, 100% 100%, 0% 100%);
  }
  100% {
    clip-path: polygon(20% 0%, 20% 0%, 20% 100%, 40% 100%, 40% 0%, 40% 0%, 40% 100%, 60% 100%, 60% 0%, 60% 0%, 60% 100%, 80% 100%, 80% 0%, 80% 0%, 80% 100%, 100% 100%, 100% 0%, 100% 0%, 100% 100%, 20% 100%);
  }
}

Star

The star transition takes advantage of how clip-path renders positive and negative space when the lines defining the shape overlap and cross each other. The shape starts as a square with eight vertices; one in each corner and one on each side. There are only three keyframes but there’s a large amount of movement in each one. The leave transition starts with the square and then moves each vertex on a side to the opposite side. Therefore, the top vertex goes to the bottom, the bottom vertex goes to the top, and the vertices on the left and right do the same swap. This creates criss-crossing lines that form a star shape in the positive space. The final keyframe then moves the vertices in each corner to the center of the shape which makes the star collapse in on itself wiping the element away. The enter transition plays the same in reverse.

.star-enter-active { animation: 1s star reverse; }
.star-leave-active { animation: 1s star; }

@keyframes star {
  0% {
    clip-path: polygon(0% 0%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 0% 100%, 0% 50%);
  }
  50% {
    clip-path: polygon(0% 0%, 50% 100%, 100% 0%, 0% 50%, 100% 100%, 50% 0%, 0% 100%, 100% 50%);
  }
  100% {
    clip-path: polygon(50% 50%, 50% 100%, 50% 50%, 0% 50%, 50% 50%, 50% 0%, 50% 50%, 100% 50%);
  }
}

Path shapes

OK, so we’ve looked at a lot of examples of animations using clip-path shape functions. One function we haven’t spent time with is path. It’s perhaps the most flexible of the bunch because we can draw custom, or even multiple, shapes with it. Chris has written and even spoken on it before.

So, while I created demo for this set of examples as well, note that clip-path paths are experimental technology. As of this writing, it’s only available in Firefox 63 or higher behind the layout.css.clip-path-path.enabled flag, which can be enabled in about:config.

See the Pen
Animating Clip-Path: Path Shapes
by Travis Almand (@talmand)
on CodePen.

This demo shows several uses of paths that are animated for transitions. The paths are the same type of paths found in SVG and can be lifted from the path attribute to be used in the clip-path CSS property on an element. Each of the paths in the demo were actually taken from SVG I made by hand for each keyframe of the animations. Much like animating with the polygon shape, careful planning is required as the number of vertices in the path cannot change but only manipulated.

An advantage to using paths is that it can consist of multiple shapes within the path, each animated separately to have fine-tune control over the positive and negative space. Another interesting aspect is that the path supports Bézier curves. Creating the vertices is similar to the polygon shape, but polygon doesn’t support Bézier curves. A bonus of this feature is that even the curves can be animated.

That said, a disadvantage is that a path has to be built specifically for the size of the element. That’s because there is no percentage-based placement, like we have with the other clip-path shapes . So, all the demos for this article have elements that are 200px square, and the paths in this specific demo are built for that size. Any other size or dimensions will lead to different outcomes.

Alright, enough talk. Let’s get to the examples because they’re pretty sweet.

Iris

The iris transition consists of four small shapes that form together to make a complete large shape that splits in an iris pattern, much like a sci-fi type door. Each shape has its vertices moved and slightly rotated in the direction away from the center to move off their respective side of the element. This is done with only two keyframes. The leave transition has the shapes move out of view while the enter transition reverses the effect. The path is formatted in a way to make each shape in the path obvious. Each line that starts with "M" is a new shape in the path.

.iris-enter-active { animation: 1s iris reverse; }
.iris-leave-active { animation: 1s iris; }

@keyframes iris {
  0% {
    clip-path: path('
      M103.13 100C103 32.96 135.29 -0.37 200 0L0 0C0.35 66.42 34.73 99.75 103.13 100Z
      M199.35 200C199.83 133.21 167.75 99.88 103.13 100C102.94 165.93 68.72 199.26 0.46 200L199.35 200Z
      M103.13 100C167.46 99.75 199.54 133.09 199.35 200L200 0C135.15 -0.86 102.86 32.47 103.13 100Z
      M0 200C68.63 200 103 166.67 103.13 100C34.36 100.12 -0.02 66.79 0 0L0 200Z
    ');
  }
  100% {
    clip-path: path('
      M60.85 2.56C108.17 -44.93 154.57 -45.66 200.06 0.35L58.64 -141.07C11.93 -93.85 12.67 -45.97 60.85 2.56Z
      M139.87 340.05C187.44 293.16 188.33 246.91 142.54 201.29C95.79 247.78 48.02 247.15 -0.77 199.41L139.87 340.05Z
      M201.68 61.75C247.35 107.07 246.46 153.32 199.01 200.5L340.89 59.54C295.65 13.07 249.25 13.81 201.68 61.75Z
      M-140.61 141.25C-92.08 189.78 -44.21 190.51 3.02 143.46C-45.69 94.92 -46.43 47.05 0.81 -0.17L-140.61 141.25Z
    ');
  }
}

Melt

The melt transition consists of two different animations for both entering and leaving. In the leave transition, the path is a square but the top side is made up of several Bézier curves. At first, these curves are made to be completely flat and then are animated downward to stop beyond the bottom of the shape. As these curves move downward, they are animated in different ways so that each curve adjusts differently than the others. This gives the appearance of the element melting out of view below the bottom.

The enter transition does much the same, except that the curves are on the bottom of the square. The curves start at the top and are completely flat. Then they are animated downward with the same curve adjustments. This gives the appearance of the second element melting into view to the bottom.

.melt-enter-active { animation: 2s melt-enter; }
.melt-leave-active { animation: 2s melt-leave; }

@keyframes melt-enter {
  0% {
    clip-path: path('M0 -0.12C8.33 -8.46 16.67 -12.62 25 -12.62C37.5 -12.62 35.91 0.15 50 -0.12C64.09 -0.4 62.5 -34.5 75 -34.5C87.5 -34.5 87.17 -4.45 100 -0.12C112.83 4.2 112.71 -17.95 125 -18.28C137.29 -18.62 137.76 1.54 150.48 -0.12C163.19 -1.79 162.16 -25.12 174.54 -25.12C182.79 -25.12 191.28 -16.79 200 -0.12L200 -34.37L0 -34.37L0 -0.12Z');
  }
  100% {
    clip-path: path('M0 199.88C8.33 270.71 16.67 306.13 25 306.13C37.5 306.13 35.91 231.4 50 231.13C64.09 230.85 62.5 284.25 75 284.25C87.5 284.25 87.17 208.05 100 212.38C112.83 216.7 112.71 300.8 125 300.47C137.29 300.13 137.76 239.04 150.48 237.38C163.19 235.71 162.16 293.63 174.54 293.63C182.79 293.63 191.28 262.38 200 199.88L200 0.13L0 0.13L0 199.88Z');
  }
}

@keyframes melt-leave {
  0% {
    clip-path: path('M0 0C8.33 -8.33 16.67 -12.5 25 -12.5C37.5 -12.5 36.57 -0.27 50 0C63.43 0.27 62.5 -34.37 75 -34.37C87.5 -34.37 87.5 -4.01 100 0C112.5 4.01 112.38 -18.34 125 -18.34C137.62 -18.34 138.09 1.66 150.48 0C162.86 -1.66 162.16 -25 174.54 -25C182.79 -25 191.28 -16.67 200 0L200 200L0 200L0 0Z');
  }
  100% {
    clip-path: path('M0 200C8.33 270.83 16.67 306.25 25 306.25C37.5 306.25 36.57 230.98 50 231.25C63.43 231.52 62.5 284.38 75 284.38C87.5 284.38 87.5 208.49 100 212.5C112.5 216.51 112.38 300.41 125 300.41C137.62 300.41 138.09 239.16 150.48 237.5C162.86 235.84 162.16 293.75 174.54 293.75C182.79 293.75 191.28 262.5 200 200L200 200L0 200L0 200Z');
  }
}

Door

The door transition is similar to the iris transition we looked at first — it’s a "door" effect with shapes that move independently of each other. The path is made up of four shapes: two are half-circles located at the top and bottom while the other two split the left over positive space. This shows that, not only can each shape in the path animate separately from each other, they can also be completely different shapes.

In the leave transition, each shape moves away from the center out of view on its own side. The top half-circle moves upward leaving a hole behind and the bottom half-circle does the same. The left and right sides then slide away in a separate keyframe. Then the enter transition simply reverses the animation.

.door-enter-active { animation: 1s door reverse; }
.door-leave-active { animation: 1s door; }

@keyframes door {
  0% {
    clip-path: path('
      M0 0C16.03 0.05 32.7 0.05 50 0C50.05 27.36 74.37 50.01 100 50C99.96 89.53 100.08 136.71 100 150C70.48 149.9 50.24 175.5 50 200C31.56 199.95 14.89 199.95 0 200L0 0Z
      M200 0C183.46 -0.08 166.79 -0.08 150 0C149.95 21.45 133.25 49.82 100 50C100.04 89.53 99.92 136.71 100 150C130.29 150.29 149.95 175.69 150 200C167.94 199.7 184.6 199.7 200 200L200 0Z
      M100 50C130.83 49.81 149.67 24.31 150 0C127.86 0.07 66.69 0.07 50 0C50.26 23.17 69.36 49.81 100 50Z
      M100 150C130.83 150.19 149.67 175.69 150 200C127.86 199.93 66.69 199.93 50 200C50.26 176.83 69.36 150.19 100 150Z
    ');
  }
  50% {
    clip-path: path('
      M0 0C16.03 0.05 32.7 0.05 50 0C50.05 27.36 74.37 50.01 100 50C99.96 89.53 100.08 136.71 100 150C70.48 149.9 50.24 175.5 50 200C31.56 199.95 14.89 199.95 0 200L0 0Z
      M200 0C183.46 -0.08 166.79 -0.08 150 0C149.95 21.45 133.25 49.82 100 50C100.04 89.53 99.92 136.71 100 150C130.29 150.29 149.95 175.69 150 200C167.94 199.7 184.6 199.7 200 200L200 0Z
      M100 -6.25C130.83 -6.44 149.67 -31.94 150 -56.25C127.86 -56.18 66.69 -56.18 50 -56.25C50.26 -33.08 69.36 -6.44 100 -6.25Z
      M100 206.25C130.83 206.44 149.67 231.94 150 256.25C127.86 256.18 66.69 256.18 50 256.25C50.26 233.08 69.36 206.44 100 206.25Z
    ');
  }
  100% {
    clip-path: path('
      M-106.25 0C-90.22 0.05 -73.55 0.05 -56.25 0C-56.2 27.36 -31.88 50.01 -6.25 50C-6.29 89.53 -6.17 136.71 -6.25 150C-35.77 149.9 -56.01 175.5 -56.25 200C-74.69 199.95 -91.36 199.95 -106.25 200L-106.25 0Z
      M306.25 0C289.71 -0.08 273.04 -0.08 256.25 0C256.2 21.45 239.5 49.82 206.25 50C206.29 89.53 206.17 136.71 206.25 150C236.54 150.29 256.2 175.69 256.25 200C274.19 199.7 290.85 199.7 306.25 200L306.25 0Z
      M100 -6.25C130.83 -6.44 149.67 -31.94 150 -56.25C127.86 -56.18 66.69 -56.18 50 -56.25C50.26 -33.08 69.36 -6.44 100 -6.25Z
      M100 206.25C130.83 206.44 149.67 231.94 150 256.25C127.86 256.18 66.69 256.18 50 256.25C50.26 233.08 69.36 206.44 100 206.25Z
    ');
  }
}

X-Plus

This transition is different than most of the demos for this article. That’s because other demos show animating the "positive" space of the clip-path for transitions. It turns out that animating the "negative" space can be difficult with the traditional clip-path shapes. It can be done with the polygon shape but requires careful placement of vertices to create the negative space and animate them as necessary. This demo takes advantage of having two shapes in the path; there’s one shape that’s a huge square surrounding the space of the element and another shape in the center of this square. The center shape (in this case an x or +) excludes or "carves" out negative space in the outside shape. Then the center shape’s vertices are animated so that only the negative space is being animated.

The leave animation starts with the center shape as a tiny "x" that grows in size until the element is wiped from view. The enter animation the center shape is a "+" that is already larger than the element and shrinks down to nothing.

.x-plus-enter-active { animation: 1s x-plus-enter; }
.x-plus-leave-active { animation: 1s x-plus-leave; }

@keyframes x-plus-enter {
  0% {
    clip-path: path('M-400 600L-400 -400L600 -400L600 600L-400 600ZM0.01 -0.02L-200 -0.02L-200 199.98L0.01 199.98L0.01 400L200.01 400L200.01 199.98L400 199.98L400 -0.02L200.01 -0.02L200.01 -200L0.01 -200L0.01 -0.02Z');
  }
  100% {
    clip-path: path('M-400 600L-400 -400L600 -400L600 600L-400 600ZM98.33 98.33L95 98.33L95 101.67L98.33 101.67L98.33 105L101.67 105L101.67 101.67L105 101.67L105 98.33L101.67 98.33L101.67 95L98.33 95L98.33 98.33Z');
  }
}

@keyframes x-plus-leave {
  0% {
    clip-path: path('M-400 600L-400 -400L600 -400L600 600L-400 600ZM96.79 95L95 96.79L98.2 100L95 103.2L96.79 105L100 101.79L103.2 105L105 103.2L101.79 100L105 96.79L103.2 95L100 98.2L96.79 95Z');
  }
  100% {
    clip-path: path('M-400 600L-400 -400L600 -400L600 600L-400 600ZM-92.31 -200L-200 -92.31L-7.69 100L-200 292.31L-92.31 400L100 207.69L292.31 400L400 292.31L207.69 100L400 -92.31L292.31 -200L100 -7.69L-92.31 -200Z');
  }
}

Drops

The drops transition takes advantage of the ability to have multiple shapes in the same path. The path has ten circles placed strategically inside the area of the element. They start out as tiny and unseen, then are animated to a larger size over time. There are ten keyframes in the animation and each keyframe resizes a circle while maintaining the state of any previously resized circle. This gives the appearance of circles popping in or out of view one after the other during the animation.

The leave transition has the circles being shrunken out of view one at a time and the negative space grows to wipe out the element. The enter transition plays the animation in reverse so that the circles enlarge and the positive space grows to reveal the new element.

The CSS used for the drops transition is rather large, so take a look at the CSS section of the CodePen demo starting with the .drops-enter-active selector.

Numbers

This transition is similar to the x-plus transition above — it uses a negative shape for the animation inside a larger positive shape. In this demo, the animated shape changes through the numbers 1, 2, and 3 until the element is wiped away or revealed. The numeric shapes were created by manipulating the vertices of each number into the shape of the next number. So, each number shape has the same number of vertices and curves that animate correctly from one to the next.

The leave transition starts with the shape in the center but is made to be unseen. It then animates into the shape of the first number. The next keyframe animates to the next number and so no, then plays in reverse.

The CSS used for this is ginormous just like the last one, so take a look at the CSS section of the CodePen demo starting with the .numbers-enter-active selector.


Hopefully this article has given you a good idea of how clip-path can be used to create flexible and powerful animations that can be both straightforward and complex. Animations can add a nice touch to a design and even help provide context when switching from one state to another. At the same time, remember to be mindful of those who may prefer to limit the amount of animation or movement, for example, by setting reduced motion preferences.

The post Animating with Clip-Path appeared first on CSS-Tricks.

Using “box shadows” and clip-path together

Let's do a little step-by-step of a situation where you can't quite do what seems to make sense, but you can still get it done with CSS trickery. In this case, it'll be applying a shadow to a shape.

You make a box

.tag {
  background: #FB8C00;
  color: #222;
  font: bold 32px system-ui;
  padding: 2rem 3rem 2rem 4rem;
}

You fashion it into a nice tag shape

You use clip-path because it's great for that.

.tag {
  /* ... */
  clip-path: polygon(30px 0%, 100% 0%, 100% 100%, 30px 100%, 0 50%)
}

You want a shadow on it, so you...

Try to use box-shadow.

.tag {
  /* ... */
  box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5);
}

But it doesn't work. Nothing shows up. You think you're going crazy. You assume you have the syntax wrong. You don't. The problem is that clip-path is cutting it off.

You can drop-shadow a parent element instead

There is a filter that does shadows as well: drop-shadow(). But you can't use it directly on the element because the clip-path will cut it off as well. So you make a parent:

<span class="tag-wrap">
  <span class="tag">
    Tag
  </span>
</span>

You can't use box-shadow on that parent either, because the parent is still a rectangle and the shadow will look wrong. But you can use filter, and the shadow will follow the shape.

See the Pen
Shadow on Shape
by Chris Coyier (@chriscoyier)
on CodePen.

That's all.

The post Using “box shadows” and clip-path together appeared first on CSS-Tricks.

8 Little Videos About the Firefox Shape Path Editor

It sometimes takes a quick 35 seconds for a concept to really sink in. Mikael Ainalem delivers that here, in the case that you haven't quite grokked the concepts behind path-based CSS properties like clip-path and shape-outside.

Here are two of my favorites. The first demonstrates animating text into view using a polygon as a clip.

The second shows how the editor can help morph one shape into another.

Direct Link to ArticlePermalink

The post 8 Little Videos About the Firefox Shape Path Editor appeared first on CSS-Tricks.

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.