The magic of clip-path
Emil Kowalski’s The Magic of Clip Path starts from a property most people file under shapes. “clip-path is often used for trimming a DOM node into specific shapes, like triangles. But what if I told you that it’s also great for animations?”
A property that skips layout#
clip-path defines a clipping region: inside is visible, outside is hidden. What makes it an animation tool is that it has no effect on layout: a clipped element occupies the same space as an unclipped one, “just like transform,” so animating it moves nothing around it.
The coordinate system starts top left. inset() takes top, right, bottom, and left offsets of a rectangle: inset(100%) hides the whole element, inset(0px 50% 0px 0px) hides its right half.
.circle { clip-path: circle(50% at 50% 50%);}The examples are all the same move. A comparison slider is two images overlaid, the top one clipped with inset(0 50% 0 0) and the value adjusted as you drag, which beats two divs with overflow hidden because it is hardware-accelerated and needs no extra DOM. The text variant splits a dashed word and a solid one at the mouse position. “It’s just a matter of creativity.”
The image reveal#
The image starts fully clipped and animates open over one second with cubic-bezier(0.77, 0, 0.175, 1), the In Out Quart curve from 2.2. You could animate height, but clip-path is hardware-accelerated, and because the image is already there and only clipped, revealing it causes no layout shift.
.image-reveal { clip-path: inset(0 0 100% 0); animation: reveal 1s forwards cubic-bezier(0.77, 0, 0.175, 1);}@keyframes reveal { to { clip-path: inset(0 0 0 0); }}To run it on scroll he uses Framer Motion’s useInView, firing once and only when at least 100px of the image is in view, and drives the animation through WAAPI to keep the logic in one place. If you are not already using Framer Motion, use Intersection Observer; the library is heavy.
const isInView = useInView(ref, { once: true, margin: "-100px" });if (isInView && ref.current) { ref.current.animate( [{ clipPath: "inset(0 0 100% 0)" }, { clipPath: "inset(0 0 0 0)" }], { duration: 1000, fill: "forwards", easing: "cubic-bezier(0.77, 0, 0.175, 1)", }, );}The scroll-progress line in the same post is the same trick: a div clipped from the bottom, with useScroll mapped through useTransform from 100% to 0%, so it looks like an SVG being drawn. Below, press Toggle and watch the edge travel; nothing fades, the panel is uncovered in place. Then turn on the opacity comparison and watch the same panel dissolve with nothing for the eye to follow.
Tabs, theme switch, and a rule#
The tabs transition solves a problem color transitions never quite do: the active tab has a different text color, and animating the color while the highlight slides always feels slightly off. His fix is to duplicate the tab list, style the duplicate as active, clip it to the active tab, and animate the clip on change; round 17px keeps the pill’s corners. The result is seamless, “and we don’t have to worry about timing the color transition, which would never be seamless anyway.” He credits a tweet by Paco Coursey and recommends Radix Tabs underneath for accessibility.
clip-path: inset(0px 75% 0px 0% round 17px);In Good vs Great Animations he revisits it as knowing your tools: slow the tabs down and the highlight and the color change fail to agree; clip-path makes them agree. Back in 2021 he used the same reveal keyframes to animate a theme switch by duplicating the whole page, which he calls hacky; the View Transitions API now does it without duplication. Vercel’s security page uses the trick, and Tuple’s comparison could. The hold-to-delete pattern from his skill file is covered in Friction.
The rule is short. When something must be revealed, wiped, or filled without the layout around it moving, reach for clip-path before height, width, or opacity.