The Mask Behind Modern Fancy CSS Borders

Creating a zig-zag, wavy, or scalloped transition between two colored sections used to mean reaching for a background-image — and then living with its rigidity. With the CSS mask property and a few gradients, however, we can build reusable, responsive borders purely in code. These aren't just for background colors either: the same technique applies to any element regardless of its content.

To understand how this works, it helps to recall what masking does. According to the CSS Masking spec, applying a mask paints the element onto the background “through” the mask, partially or completely hiding parts of it. Since mask accepts an image, and gradients are images, we have a full toolkit for shaping elements with nothing more than CSS gradient syntax.

Consider a basic case: a gradient that fades an image out, versus one with a hard color stop that cleanly clips half of it away. That second approach — using a hard stop — is exactly how we create the illusion of fancy borders. And because mask supports multiple comma-separated gradients, we can clip an element from multiple angles or sides simultaneously.

One important detail: to hide parts of the image, we use a transparent black value (#0000); to reveal parts, full black (#000). The gradients themselves never rely on matching the page background.

Zig-Zag Borders

A zig-zag bottom border starts with a single conic-gradient() placed at a fixed size in the center. By removing no-repeat, that same gradient tiles across the width, producing the repeating angular peaks. Gradients, especially in Chrome, occasionally render with jagged edges, so a slight transition — from blue 90deg, green 0 to green, blue 1deg 89deg, green 90deg — smooths the result.

There are two variables behind the shape: size (e.g., 80px) and angle (e.g., 90deg). Both can be expressed as CSS custom properties for easy adjustments:

With those variables in hand, the principle repeats for each side. The top border uses nearly the same code, but swaps bottom for top and adjusts gradient rotation to 180deg - angle/2 instead of -angle/2. Left and right borders follow the same pattern with rotation and positioning changes.

--size: 80px;
--angle: 90deg;

mask:
  conic-gradient(
    from calc(var(--angle)/-2) at bottom,
    #0000, #000 1deg calc(var(--angle) - 1deg), #0000 var(--angle)
  ) 50%/var(--size) 100%;

Applying zig-zag borders to two sides at once — top/bottom or left/right — simply means combining the one-sided code. For an all-sides configuration, the shape becomes trickier; those cases require fixed angles rather than arbitrary ones. Implementations for those are available as Zig-Zag boxes.

mask:
  conic-gradient(
    from {-angle/2} at bottom,
    #0000, #000 1deg {angle - 1}, #0000 {angle}
  ) bottom/{size} 51% repeat-x;
  conic-gradient(
    from {180deg - angle/2} at top, 
    #0000, #000 1deg {angle - 1}, #0000 {angle}
  ) top   /{size} 51% repeat-x;

Scooped Borders

Switching from zig-zag to scooped borders is surprisingly simple. The structure stays identical; only the gradient type changes. Replace conic-gradient() with a radial-gradient(), and the angles disappear entirely — only the size variable remains.

mask: radial-gradient({size} at bottom,#0000 98%,#000) 50% / {1.85*size} 100%;

Two magic numbers appear in this approach: 1.85 and 98%. Logically the circle should touch the full height of the background area, hence 100%, but anti-aliasing again causes jagged edges, so 98% prevents overlapping artifacts. The 1.85 value is more preference than necessity: while 2 theoretically creates a perfect circle, slightly reducing this yields a smoother visual overlap between repeated circles.

As before, the one-sided code is reused for two sides. The top/bottom and left/right configurations each need only a single gradient. The all-sides version, called a Pointy box, does require multiple gradients — and a round() function to ensure the width remains a clean multiple of the size variable.

Scalloped Borders

Unlike the previous shapes, every scalloped border configuration needs two gradients. A radial gradient creates the repeated circles, while a linear gradient masks them to reveal only the sides you want.

The space value in the background repetition is critical here. It guarantees circles aren’t clipped at the edges without requiring additional gradients. The 1.85 value again personalizes overlap for a cleaner finish.

Wavy Borders

The wavy border falls somewhere between zig-zag and scooped shapes. Building a bottom wave starts by defining a single wave unit — essentially a mini illustration of one hump — then repeating it along that side's length. The same gradient stack, with only minor variable changes, generates distinct wave styles for top, bottom, left, and right. The all-sides version exists as a Wiggly box for those wanting the full treatment. Complex variations of wavy shapes are covered in more depth in a dedicated guide on wavy shapes.

mask: 
  radial-gradient({size} at 75% 100%,#0000 98%,#000) 50% calc(100% - {size})/{4*size} 100% repeat-x,
  radial-gradient({size} at 25% 50% ,#000 99%,#0000 101%) bottom/{4*size} {2*size} repeat-x;

The takeaway here is that you no longer need an online generator to copy-paste from, though it can still be a handy tool. By understanding how gradient masks stack and clip, you gain the flexibility to customize every parameter — size, angle, smoothness, sides — directly in your stylesheet. Fancy borders that previously demanded raster images now live purely in CSS, and changing the look is often just a matter of a value swap.