Scroll-Driven corner-shape: A Practical Look

Two frontend features are converging in ways that deserve attention. First, scroll-driven animations, which tie an animation’s timeline to the scroll position, are gaining ground and are slated for inclusion in Interop 2026, making them Baseline once Firefox support lands. Second, the CSS corner-shape property, currently Chrome-only, brings mathematically defined corners to the browser, making shapes like notches and squircles trivial to create. Because both features are built on interpolation-friendly values, they combine quite naturally.

Since corner-shape is mathematical, it can be animated smoothly. The property accepts keyword values like squircle and notch, and also the superellipse() function, which underpins those keywords. For instance, superellipse(2) yields the squircle shape. The ability to go from one superellipse value to another is what makes scroll-driven corner-shape animations possible.

Understanding The Core Mechanics

Here’s a quick reference for the different corner-shape values:

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.

A foundational detail: border-radius doesn’t actually round corners. Under the hood, it’s corner-shape: round that does the visual rounding, while border-radius supplies the x and y coordinates that define the shape’s extent. In the examples below, we’ll override that implicit corner-shape within keyframes.

A practical way to set up a scroll-driven effect involves using a pseudo-element fixed to the viewport edges:

@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;
}

Here, content: "" creates the pseudo-element, which is fixed in place and sits atop the content, so pointer-events: none keeps the underlying page interactive. The color uses mix-blend-mode: difference with background: white to invert the backdrop — a visually striking approach, but one that trades off full color contrast. A utility class can opt elements out of this effect when needed:

/* Added to cards */
.no-filter {
  isolation: isolate;
}

You can compare the difference visually:

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: Full application of blend mode. Right: Blend mode excluded from cards.

To animate, you typically use a large radius like border-bottom-left-radius: 100%, which shifts the drawing coordinates to the opposite ends of their axes. The scroll-driven nature is set through animation-timeline: scroll(), eliminating the need for a duration. Within the keyframes, you interpolate between values like corner-shape: superellipse(notch)—an inset square that looks sharply cornered, equivalent to superellipse(-infinity)—and corner-shape: superellipse(square), an outset square represented by superellipse(infinity).

Refining the Animation Peripherals

The initial approach has a visual flaw: the curvature feels harsh at the start and end, and the shape appears to be pulled into the viewport corners. A more elegant result comes from extending the shape beyond the viewport’s edges:

/* Change this... */
inset: 0;

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

While this stretching causes the animation to appear delayed and truncated, you can compensate by avoiding the extreme infinity values:

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

  to {
    corner-shape: superellipse(6);
  }
}

Part of the shape will remain visible, but tuning the superellipse() parameters keeps the bulk of the animation outside the visible frame, resulting in a smoother visual arc. The side-by-side difference is striking:

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.

Illustrating the aspect ratio implications and the geometry at work:

/* 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.

Integration With Other Scroll Features

Scroll-driven animations coexist comfortably with other scrolling mechanics, whether using native CSS features or basic JavaScript methods like scrollTo(), scrollBy(), or scrollIntoView(). Adding scroll snapping to the mix requires just a few more lines:

:root {
  /* Snap vertically */
  scroll-snap-type: y;

  section {
    /* Snap to section start */
    scroll-snap-align: start;
  }
}

Corner-Shape as an Inverted Mask

A different pattern uses corner-shape not just as an animated element, but as a mask. In the following setup, a border is drawn around the viewport, then a notched shape with background: inherit sits on top, fully obscuring that border until scroll reveals it. Adding a small rotation, rotate: 5deg, makes the intersecting geometry more apparent:

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 time, the animation targets border-radius instead of corner-shape. Animating to border-radius: 20vw / 20vh expands the corner coordinates along the x- and y-axes, progressively unveiling the decorative border underneath. Paying attention to z-index ensures the content stays above the shape layer:

@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 Elements

Beyond individual shapes, corner-shape works well across several elements at once. Nested diamond shapes can be created with corner-shape: bevel and all driven by the same scroll-linked animation, growing outward via padding:

<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;
}

There are many possible directions beyond simple keyword-to-keyword animation. Interpolating custom superellipse() values, repurposing shapes as masks, and orchestrating several synchronized elements all scale naturally into scroll-driven effects, though the same techniques would produce equally interesting outcomes as static designs.