AI & ML

Exploring Scroll-Driven Corner Shape Animations: A Technical Deep Dive

· 5 min read

Scroll-driven animations have captured developers' attention over the past few years, and they're about to become baseline once Firefox adds support without requiring a flag. As part of Interop 2026, that milestone should arrive soon. The concept is elegant: an animation timeline's progress syncs directly with scroll position—scroll halfway down the page, and you're halfway through the animation. Best of all, implementation is straightforward.

Another CSS feature generating buzz is the corner-shape property, currently Chrome-only. It unlocks corner styles beyond simple rounding, enabling distinctive geometric shapes with minimal code. The real power lies in its mathematical foundation, making it perfectly suited for animation.

Combine these two capabilities and you get scroll-driven corner-shape animations (Chrome 139+ required):

corner-shape fundamentals

The corner-shape property accepts keyword values that correspond to superellipse() function outputs:

corner-shape keywordsuperellipse() equivalent
squaresuperellipse(infinity)
squirclesuperellipse(2)
roundsuperellipse(1)
bevelsuperellipse(0)
scoopsuperellipse(-1)
notchsuperellipse(-infinity)
Showing the same magenta-colored rectangle with the six difference CSS corner-shape property values applied to it in a three-by-three grid.

These keywords are shortcuts for the underlying superellipse() function, which uses mathematical equations to generate corner shapes. For instance, superellipse(2) produces the "squircle"—a shape between square and circle. Since the function is mathematical at its core, it's inherently animatable, which opens up the possibilities we're exploring here.

Building the animation

Here's the complete CSS for the effect, followed by a breakdown:

@keyframes bend-it-like-beckham {
from {
corner-shape: superellipse(notch);
/* or */
corner-shape: superellipse(-infinity);
}
to {
corner-shape: superellipse(square);
/* or */
corner-shape: superellipse(infinity);
}
}
body::before {
/* Fill viewport */
content: "";
position: fixed;
inset: 0;
/* Enable click-through */
pointer-events: none;
/* Invert underlying layer */
mix-blend-mode: difference;
background: white;
/* Don't forget this! */
border-bottom-left-radius: 100%;
/* Animation settings */
animation: bend-it-like-beckham;
animation-timeline: scroll();
}
/* Added to cards */
.no-filter {
isolation: isolate;
}

The body::before pseudo-element with content: "" creates an empty layer that's fixed to the viewport edges via inset: 0. Since this animated shape sits above the content, pointer-events: none ensures users can still interact with underlying elements.

The visual effect uses mix-blend-mode: difference with a white background to invert colors beneath it—a popular technique that maintains reasonable contrast in most cases. To exclude specific elements from this effect, apply this utility class:

/* Added to cards */
.no-filter {
isolation: isolate;
}
Side-by-side comparison showing blend mode applied on the left and excluded from cards placed in the layout on the right, preventing the card backgrounds from changing.
Left: Blend mode applied globally. Right: Cards isolated from the effect.

The corner-shape property requires border-radius to function. Here's an important detail: border-radius doesn't actually round corners—it defines the x and y coordinates for the corner shape, while corner-shape: round (the default) handles the actual rounding:

/* Syntax */
border-bottom-left-radius: <x-axis-coord> <y-axis-coord>;
/* Usage */
border-bottom-left-radius: 50% 50%;
/* Or */
border-bottom-left-radius: 50%;
Diagramming the shape showing border-radius applied to the bottom-left corner. The rounded corner is 50% on the y-axis and 50% on the x-axis.

We're using border-bottom-left-radius: 100% to position those coordinates at the far end of each axis. The @keyframes animation overrides the default corner-shape: round, referenced via animation: bend-it-like-beckham. No duration is needed since animation-timeline: scroll() ties the animation to scroll progress.

The keyframes animate from corner-shape: superellipse(notch) (an inset square, equivalent to superellipse(-infinity)) to corner-shape: superellipse(square) (an outset square, or superellipse(infinity)).

Refining the animation

The initial demo has a subtle issue worth addressing. At the animation's start and end, the curvature appears harsh because we're using the extreme notch and square values. The shape also seems to get pulled into the corners, and being constrained to the viewport edges feels restrictive.

The fix is simple:

/* Change this... */
inset: 0;
/* ...to this */
inset: -1rem;

This extends the shape beyond the viewport boundaries. While this makes the animation appear to start late and end early, we can compensate by avoiding the extreme -infinity and infinity values:

@keyframes bend-it-like-beckham {
from {
corner-shape: superellipse(-6);
}
to {
corner-shape: superellipse(6);
}
}

This keeps part of the shape visible throughout, but adjusting the superellipse() value ensures it stays outside the viewport when needed. Here's the difference:

Two versions of the same magenta colored rectangle side-by-side. The left shows the top-right corner more rounded than the right which is equally rounded.

And here's the refined version:

Expanding scroll capabilities

Scroll-driven animations integrate seamlessly with other CSS scroll features, including scroll snapping, scroll buttons, scroll markers, text fragments, and JavaScript scroll methods like scrollTo(), scroll(), scrollBy(), and scrollIntoView().

Adding scroll snapping to complement an existing scroll-driven corner-shape animation requires just a few lines of CSS:

:root {
/* Snap vertically */
scroll-snap-type: y;
section {
/* Snap to section start */
scroll-snap-align: start;
}
}

Creating masks with corner-shape

Here's a technique that uses corner-shape as a masking layer. By placing a border around the viewport and overlaying a notched shape (corner-shape: notch) with the same background color (background: inherit), the shape initially conceals the border completely. As you scroll, the animation progressively reveals the border's four corners:

Making the shape more visible reveals an additional detail: the shape rotates slightly (rotate: 5deg), adding visual interest to the effect.

A large gray cross shape overlaid on top of a pinkish background. The shape is rotated slightly to the right and extends beyond the boundaries of the background.,

This implementation animates border-radius rather than corner-shape directly. The animation target of border-radius: 20vw / 20vh uses viewport-relative units where 20vw and 20vh control the x-axis and y-axis curvature of each corner. This reveals 20% of the border as scrolling progresses.

One implementation detail: proper z-index management ensures content appears above both the border and the masking shape. Beyond that consideration, this demonstrates another creative application of corner-shape:

@keyframes tech-corners {
from {
border-radius: 0;
}
to {
border-radius: 20vw / 20vh;
}
}
/* Border */
body::before {
/* Fill (- 1rem) */
content: "";
position: fixed;
inset: 1rem;
border: 1rem solid black;
}
/* Notch */
body::after {
/* Fill (+ 3rem) */
content: "";
position: fixed;
inset: -3rem;
/* Rotated shape */
background: inherit;
rotate: 5deg;
corner-shape: notch;
/* Animation settings */
animation: tech-corners;
animation-timeline: scroll();
}
main {
/* Stacking fix */
position: relative;
z-index: 1;
}

Animating multiple corner-shape elements

This demonstration features nested diamond shapes created with corner-shape: bevel. All diamonds share a single scroll-driven animation that expands their size through padding adjustments:

<div id="diamonds">
<div>
<div>
<div>
<div>
<div>
<div>
<div>
<div>
<div>
<div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<main>
<!-- Content -->
</main>
@keyframes diamonds-are-forever {
from {
padding: 7rem;
}
to {
padding: 14rem;
}
}
#diamonds {
/* Center them */
position: fixed;
inset: 50% auto auto 50%;
translate: -50% -50%;
/* #diamonds, the <div>s within */
&, div {
corner-shape: bevel;
border-radius: 100%;
animation: diamonds-are-forever;
animation-timeline: scroll();
border: 0.0625rem solid #00000030;
}
}
main {
/* Stacking fix */
position: relative;
z-index: 1;
}

Wrapping up

These examples demonstrate animating between custom superellipse() values, using corner-shape as a masking technique to generate novel shapes, and coordinating animations across multiple corner-shape elements simultaneously. The possibilities extend far beyond simple keyword transitions—especially when combined with scroll-driven animations to create compelling interactive effects. These techniques work equally well as static designs or dynamic scroll-based experiences.


Experimenting With Scroll-Driven corner-shape Animations originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.