Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Mon, 13 Oct 2025 18:16:19 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 Modern CSS Round-Out Tabs https://frontendmasters.com/blog/modern-css-round-out-tabs/ https://frontendmasters.com/blog/modern-css-round-out-tabs/#comments Mon, 13 Oct 2025 15:56:32 +0000 https://frontendmasters.com/blog/?p=7381 Quite a while back I made a set of “round out” tabs, where the literal tab part of the UI would connect to the content below with a rounded edge that flared out as it connected. A bit tricky of a situation, even now!

That old school solution used four additional elements per tab. Two to place a square on the bottom edges of the tab, and then larger circles to hide everything but the flared part.

Illustration showing a tab design with rounded edges, featuring a central tab with additional shapes for visual effects. The background consists of different shades and shapes, emphasizing the tab structure.

Here’s that (again: old!) demo:

Let’s Use shape() Instead

I’m so hyped on shape(). It’s an amazing addition to CSS, giving us a primitive that can draw, well, anything you could draw with a pen tool.

In our case we’re going to use the shape() primitive with clip-path to carve a tab shape out of a rectangle. No extra elements!

.tab {
  clip-path: shape(
    /* do commands to cut out a tab shape */
  );
}

The shape() function takes all these commands to do the drawing. Depending on how complex a thing you are trying to do, the syntax is fairly human-readable.

Let’s slowly walk through hand-building this tab shape. It’ll be extra cool because:

  1. It’s not completely fixed shape. Parts of it can be fixed coordinates, and other parts can be flexible. You’ll see, it’s awesome.
  2. We can variablize it, meaning we can adjust the look on the fly.

1) Starting Out!

Elements start out as rectangles. Ours are going to be horizontally longer rectangles just by virtue of them having text in them pushing them that direction. Then a bit of padding pushing those inline edges more than the block side edges.

.tab {
  display: inline-block; /* So <a> will take padding */
  padding: 0.5rem 2rem;
  white-space: nowrap; /* a wrapped tab will look silly */
  
  clip-path: shape(
    from bottom left,
  );
}

We wanna start carving away at this tab with clip-path from the bottom left corner, so here we go.

2) The First Curve!

Right away we need to curve into the tab shape. This is beautiful right away, as this is the “round out” part that is hard to pull off. Ain’t no border-shape can really help us here, we’re fancy people.

.tab {
  ...
  
  clip-path: shape(
    from bottom left,
    curve to 10px calc(100% - 10px) with 10px 100%,
  );
}

3) Straight up!

We could use syntax (line) here saying “draw a straight line to these new coordinates”, but I think it’s more satisfying here to use syntax (vline) saying “whatever horizontal coordinate you’re at doesn’t matter, just draw to this new vertical coordinate”.

.tab {
  ...
  
  clip-path: shape(
    from bottom left,
    curve to 10px calc(100% - 10px) with 10px 100%,
    vline to 10px
  );
}

4) Curve to the Top!

We’ll use the same curve command here as the first curve, where we specify where we’re going and a point the curve should use to sorta pull toward.

Honestly I tried using arc commands here first (like arc to 20px 10px of 20%) but by default the arc curved “the wrong way” making a bite shape and I didn’t really get what 20% meant. I’m absolutely sure it’s possible and maybe a smidge easier, I just thought curve made more sense to me.

.tab {
  ...
  
  clip-path: shape(
    from bottom left,
    curve to 10px calc(100% - 10px) with 10px 100%,
    vline to 10px,
    curve to 20px 0 with 10px 0,
  );
}

5) Moving to the Other Side!

This is my favorite point on the whole shape.

Again instead of specifying an exact coordinate, we’re just saying draw horizontally from wherever you are to 20px away from the right edge.

We don’t know how far away the last point and this new point are away from each other. They could be 200px away, 117.23px away, 0px away, the line could even draw to the left because the element is so narrow. That’s good. We’re drawing a shape here with points that are a combination of fixed positions (e.g. 10px from the top!) and flexible positions (20px away from whatever the right edge is!).

.tab {
  ...
  
  clip-path: shape(
    from bottom left,
    curve to 10px calc(100% - 10px) with 10px 100%,
    vline to 10px,
    curve to 20px 0 with 10px 0,
    hline to calc(100% - 20px),
  );
}

6) Draw the Rest of the Owl

From here, I think you get the point. We’re going to:

  1. Curve back downward.
  2. Draw the vertical line.
  3. Curve to complete the round-out.

We don’t need to draw a line back to the start of the shape. That’s just implied magically.

.tab {
  ...
  
  clip-path: shape(
    from bottom left,
    curve to 10px calc(100% - 10px) with 10px 100%,
    vline to 10px,
    curve to 20px 0 with 10px 0,
    hline to calc(100% - 20px),
    curve to calc(100% - 10px) 10px with calc(100% - 10px) 0,
    vline to calc(100% - 10px),
    curve to 100% 100% with calc(100% - 10px) 100%
  );
}

That complete’s our shape! The white areas here are what is “cut away” leaving the yellow area (just for visualization):

The padding we’ve set in the inline direction (2rem) is plenty to survive from being clipped away, as we’re only clipping ~10px away.

Variablizing Things

Hmmmmmmm.

Notice we used 10px and awful lot in our shape(). We used a couple of 20px values too, and the intention was clearly “twice as much as that other value”. So we could get away with setting a custom property to 10px and using that repetitively.

.tab {
  --tabGirth: 12px;

  clip-path: shape(
    from bottom left,
    curve to var(--tabGirth) calc(100% - var(--tabGirth)) with
      var(--tabGirth) 100%,
    vline to var(--tabGirth),
    curve to calc(var(--tabGirth) * 2) 0 with var(--tabGirth) 0,
    hline to calc(100% - calc(var(--tabGirth) * 2)),
    curve to calc(100% - var(--tabGirth)) var(--tabGirth) with
      calc(100% - var(--tabGirth)) 0,
    vline to calc(100% - var(--tabGirth)),
    curve to 100% 100% with calc(100% - var(--tabGirth)) 100%
  );
}

The Modern Demo

I added a few doo-dads to the final demo. The hover and active states push the tabs down a little with translate, for instance. That’s nothing to write home about, but then I wanted to rudimentary overflow: auto behavior so the non-wrapping tabs didn’t blow out horizontally, and it led to this:

The horizontal scrollbar is what I wanted, but the vertical scrollbar is like: no.

So I enjoyed the fact that can now (sorta) do single-directional overflow control:

/*
  Allow horizontal scrollbars, but
  hide vertical overflow
*/
overflow-inline: auto;
overflow-block: clip;

I also used Knobs to give a UI control to the CSS variable --tabGirth so you can see how the tabs look with different values. The more girth almost the smaller the tabs look, because we need to “cut away” more of the tab.

There is a smidge of other trickery in there like getting shadows via filter on a parent element, that even work with the clip-path.

Fallbacks

Not every browser supports shape() at the time of this writing (there is even sub-support issues of syntax features).

But that doesn’t mean we have to deliver them entirely rectangular tabs. A @supports test allows us to deliver a fallback just fine. We just need to pass in a valid shape syntax (you can’t just do shape()).

.tab {
  ...
  
  @supports not (clip-path: shape(from top left, hline to 0)) {
    /* less padding needed inline */
    padding-inline: 1rem; 
    
    /* top rounding */
    border-start-start-radius: var(--tabGirth);
    border-start-end-radius: var(--tabGirth);
  }
}

Accessibility of Tabs

The tabs are built from anchor links that jump-link to the related content. When JavaScript is active, they get what I think are the correct roles and aria-* attributes. The aria-attributes are updated when I think is the appropriate time to the appropriate values.

But I’m sure this isn’t fully correct. Just having anchor links here means the arrow keys to change tabs don’t work, which I think is a general requirement of tabs. So anyway this is mostly about the design of the tabs and you’d be better off consulting elsewhere for perfectly accessible implementations of the behavior.

Other Examples

I looked around at a number of older examples and a lot of them involve pseudo or extra elements and have aged like milk. Despite the modern browser support requirements here, I expect the above will age much better, as will these more modern takes below:

]]>
https://frontendmasters.com/blog/modern-css-round-out-tabs/feed/ 2 7381
The `-path` of Least Resistance (Part 1) https://frontendmasters.com/blog/the-path-of-least-resistance-part-1/ https://frontendmasters.com/blog/the-path-of-least-resistance-part-1/#respond Wed, 27 Aug 2025 18:58:53 +0000 https://frontendmasters.com/blog/?p=6758 There’s a whole layer of CSS that lives just below the surface of most interfaces. It’s not about layout, spacing, or typography. It’s about shape. About cutting through the default boxes and letting your UI move in new directions. This series is all about one such family of features, the kind that doesn’t just style your layout but gives you entirely new ways to shape, animate, and express your interface.

In this first part, we’ll explore clip-path. We’ll start simple, move through the functions and syntax, and work our way up to powerful shape logic that goes way beyond the basic polygons you might be used to. And just when you think things can’t get any more dynamic, part two will kick in with offset-path, where things really start to move.

Article Series

What is clip-path in CSS?

At its core, clip-path lets us control which parts of an element are visible. It’s like a stencil or cookie cutter for HTML elements. Instead of displaying a rectangular box, you can show just a circle, a triangle, a star, or any complex shape you define. And you can do it with a single line of CSS.

This opens the door to more expressive designs without relying on images, SVG wrappers, or external tools. Want to crop a profile picture into a fancy blob shape? Easy. Want to reveal content through a custom cutout as a hover effect? Done. That’s exactly where clip-path shines. But to use it effectively, we need to understand what it’s made of.

Before the syntax

To really get clip-path, let’s break it into two basic concepts: clip and path. No joke, each one of those carries an important lesson of its own.

This is not the “clip” you know

We’ve all seen clipping in CSS before, usually through the overflow property, set to hidden or clip. By doing so, anything that spills out of the element’s box just vanishes.

But here’s the key difference. While the overflow property clips the content of the element (on the padding box for hidden, and on the overflow clip edge for clip), the clip-path property clips the element itself.

This means that even the simplest clip-path, which visually mimics overflow clipping, will still hide parts of the element itself. That includes things like a box-shadow you were expecting to see, or an outline on a button that suddenly disappears and breaks accessibility.

Also worth noting: just like overflow, clip-path lives entirely in two dimensions. No depth, no perspective. It flattens everything. That means transform-style: preserve-3d is ignored, and any 3D motion will stay locked to the element’s plane.

The “path” to success

This one trips people up. Especially when you’re working with functions like polygon(), it’s tempting to think of the shape as just a bunch of points. But it’s not just the points that matter, it’s the order they come in. You’re not dumping coordinates into a bucket, you’re connecting them, one by one, like a game of “connect the dots.”

A connect-the-dots illustration of a dinosaur character, featuring numbered dots in a sequence, set against a black background.

The path is the journey from one point to the next. The way you sequence them defines the outline, the curves, and eventually the clipped shape. If the points are out of order, your shape won’t behave the way you expect.

Values and Coordinates

You can set the coordinates for your shapes in absolute units like pixels, which stay fixed regardless of the element’s size, or in relative units like percentages, which adapt based on the element’s dimensions. Absolute values give you precision, while relative values make your shapes more responsive. In practice, you’ll often mix the two to balance consistency and flexibility.

By default, every shape you define with clip-path is calculated relative to the element’s border-box. This means the point 0 0 sits at the top-left corner of that box, and all coordinates extend from there. Positive X values move to the right, and positive Y values move down.

Note that you’re not limited to the border-box; the clip-path property also accepts an optional <geometry-box> value, which lets you choose the reference box for your shape, giving you more control over how the clip is applied.

Basic Shapes

Let’s begin with the simplest shape of all. The circle() function creates a circular clipping path that allows you to cut content into a perfect circle shape. This function accepts two main parameters: the radius of the circle and its center position.

The basic syntax follows this pattern:

clip-path: circle(radius at position);

The radius can be specified in various units, like pixels (px), percentages (%), or viewport units (vw, vh). The position defines where the center of the circle should be placed, using coordinates relative to the element’s dimensions.

This demo shows a live preview of the circle() function in action. You can drag the control nodes to adjust both the center position and radius of the circular clip path. As you manipulate these controls, you’ll see the clipped area update in real time, and the corresponding CSS values will be displayed below the preview.

Use the checkbox to toggle between pixel and percentage values to see how the result can be expressed in different units. This is particularly useful when you need responsive clipping that adapts to different screen sizes.

Using Keywords

Beyond specific coordinate values, CSS also supports several convenient keywords for positioning the circle’s center. You can use keywords like center, top, bottom, left, and right, or combine them for more precise placement, such as top left or bottom right. These keywords provide a quick way to achieve common positioning without calculating exact pixel or percentage values.

You can also use special keywords for the radius: closest-side and farthest-side. The closest-side keyword sets the radius to the distance from the center to the closest edge of the element, while farthest-side extends the radius to the farthest edge.

For example:

clip-path: circle(50px at left);
clip-path: circle(30% at top right);
clip-path: circle(closest-side at top 25%);
clip-path: circle(farthest-side at center);

Slightly stretched: ellipse()

Now let’s take that circle and give it two radii instead of one. The ellipse() function works similarly to circle(), but instead of creating a perfect circle, it produces an oval shape by accepting two separate radius values. This gives you independent control over both the horizontal and vertical dimensions of the clipping shape.

The syntax extends the circle pattern with an additional radius parameter:

clip-path: ellipse(radiusX radiusY at position);

This demo shows the ellipse() function with three control nodes, that allow you to independently adjust the horizontal and vertical radii. Notice how you can create anything from a wide, flat oval to a tall, narrow shape by manipulating these controls separately.

Rectangular Shapes

While circle() and ellipse() create curved clipping paths, CSS also provides several functions for creating rectangular clips. These functions offer different approaches to defining the same basic shape: a rectangle with straight edges.

inset(), rect(), and xywh()

These three are all about boxes, but each one approaches it differently.

  • inset() defines distances to clip inward from each edge. Its like padding in reverse, instead of adding space inside the box, you remove it.
  • rect() uses absolute coordinates from the top-left corner to define the rectangle’s edges. A legacy function from the old clip property, but still valid and supported in CSS.
  • xywh() defines a rectangle by position and size. The first two values set the X and Y coordinates for the top-left corner, and the next two define the width and height. Clean and straightforward.

This demo lets you compare all three rectangular functions using the same visual controls. Drag the red control lines to adjust the clipping boundaries, and use the dropdown to switch between the different function syntaxes. Notice how the same visual result produces different coordinate values depending on which function you choose.

The inset() function is generally the most intuitive since it works similarly to CSS padding, while rect() follows the traditional clipping rectangle approach. The newer xywh() function uses a more familiar x, y, width, height pattern commonly found in graphics programming.

Now for the fun part: polygon()

Here’s where things get interesting. While circles, ellipses, and rectangles are useful, they’re also predictable. The polygon() function is where you start building custom shapes, point by point, corner by corner.

At its heart, polygon() is wonderfully straightforward. You define a series of coordinate pairs, and CSS connects them in order to create your shape:

clip-path: polygon(x1 y1, x2 y2, x3 y3, ...);

Remember when we talked about the “path” concept earlier? This is where it really shows. Each coordinate pair is a waypoint, and CSS draws straight lines between them in the exact sequence you provide. Here’s a perfect example of why order matters. Take these five points:

/* Pentagon-like shape */
clip-path: polygon(50% 0%, 98% 35%, 79% 91%, 21% 91%, 2% 35%);

/* Same points, different order - creates a star */
clip-path: polygon(50% 0%, 79% 91%, 2% 35%, 98% 35%, 21% 91%);

Same coordinates, completely different shapes. The first creates a neat pentagon-like outline, while the second forms a classic five-pointed star. It’s that simple connection from point to point that builds your final shape.

Polygon Builder

Here’s a demo that lets you create and modify polygons in real time. You can drag the red control nodes to reshape your polygon, add or remove points, and see the resulting CSS code update instantly. Toggle the checkbox to switch between pixel and percentage values for responsive design.

Use the “Add Node” button to introduce new points along your polygon’s edges, or “Remove Node” to simplify the shape. Notice how each modification creates a completely new path—and how the order of your points defines the final appearance.

When Straight Lines Aren’t Enough

Polygons are powerful, but they have one fundamental limitation: they’re made entirely of straight lines. Sometimes your design calls for curves, smooth transitions, or complex shapes that can’t be achieved by connecting points with straight edges. That’s where path() and shape() step in.

path(): Raw Power, Borrowed from SVG

The path() function brings the full power of SVG path syntax directly into CSS. If you’ve ever worked with vector graphics, this will feel familiar. The syntax is identical to SVG’s <path> element:

clip-path: path("M 10,10 L 50,10 L 50,50 Z");

You can use any SVG path command: M for move, L for line, C for cubic curves, Q for quadratic curves, and so on. This gives you incredible precision and the ability to create complex shapes with smooth curves and sharp angles exactly where you want them.

If you’re not comfortable writing path commands by hand, there are plenty of free online SVG path editors like SVG Path Editor or Boxy SVG that can generate the path string for you.

Here’s a simple heart shape as an example:

clip-path: path("M100,178 L87.9,167 C45,128 16.7,102 16.7,71 C16.7,45 37,25 62.5,25 C77,25 90.9,32 100,42 C109.1,32 123,25 137.5,25 C163,25 183.3,45 183.3,71 C183.3,102 155,128 112.1,167 Z");

But here’s the catch: because path() comes from the SVG world, it only works with absolute values. There are no percentages, no responsive units. If your element changes size, your clipping path stays exactly the same. For truly flexible, responsive shapes, we need something more modern.

shape(): The Modern Approach

Enter shape() – CSS’s answer to the limitations of path(). It provides the same curve capabilities as path() but with a more CSS-friendly syntax and support for relative units like percentages.

Here’s the same heart shape, but using shape() with relative coordinates:

clip-path: shape(
  from 50% 89%,
  line to 43.95% 83.5%,
  curve to 8.35% 35.5% with 22.5% 64% / 8.35% 51%,
  curve to 31.25% 12.5% with 8.35% 22.5% / 18.5% 12.5%,
  curve to 50% 21% with 38.5% 12.5% / 45.45% 16%,
  curve to 68.75% 12.5% with 54.55% 16% / 61.5% 12.5%,
  curve to 91.65% 35.5% with 81.5% 12.5% / 91.65% 22.5%,
  curve to 56.05% 83.5% with 91.65% 51% / 77.5% 64%,      
  close);

This demo shows the same heart shape created with both methods. The key difference becomes apparent when you resize the containers. Grab the bottom-right corner of each shape and drag to change its size.

Notice how the path() version maintains its fixed pixel dimensions regardless of the container size, while the shape() version scales proportionally thanks to its percentage-based coordinates. This responsiveness is what makes shape() particularly powerful for modern web design and represents the future of CSS clipping paths.

Syntax Table

If you’re coming from an SVG background, you’ll find the transition to shape() remarkably intuitive. The syntax translates beautifully from SVG path commands, maintaining the same logic while embracing CSS’s flexible unit system.

Just as SVG paths distinguish between absolute (uppercase) and relative (lowercase) commands, shape() uses the keywords to and by. Commands with to are positioned relative to the element’s origin, while commands with by are positioned relative to the previous point in the path.

SVG PathShape EquivalentDescription
M/mfromSet first point
M 10 20
m 10 20
move to 10px 20px
move by 10px 20px
Move point
L 30 40
l 30 40
line to 30px 40px
line by 30px 40px
Draw line
H 50
h 50
hline to 50px
hline by 50px
Horizontal line
V 60
v 60
vline to 60px
vline by 60px
Vertical line
C x1 y1 x2 y2 x y
c x1 y1 x2 y2 x y
curve to x y with x1 y1 / x2 y2
curve by x y with x1 y1 / x2 y2
Cubic curve with two control points
S x1 y1 x y
s x1 y1 x y
curve to x y with x1 y1
curve by x y with x1 y1
Cubic curve with one control point
Q x1 y1 x y
q x1 y1 x y
smooth to x y with x1 y1
smooth by x y with x1 y1
smooth curve with one control point
T x y
t x y
smooth to x y
smooth by x y
smooth curve with no control point
A rx ry angle la sw x y
a rx ry angle la sw x y
arc to x y of rx ry sw la angle
arc by x y of rx ry sw la angle
Arc with radii, rotation, and flags
Z/zcloseClose the path

Self-Intersecting Polygons and Fill Rules

Here’s where things get mathematically interesting. When you create shapes where lines cross over each other, CSS has to decide which areas should be visible and which should remain transparent. This is controlled by fill rules, and understanding them unlocks some powerful creative possibilities.

CSS supports two fill rules: evenodd and nonzero. The difference becomes clear when you see them in action. Here’s a simple rounded star with both fill rules:

  • Even-odd rule: (on the left) Think of it as a simple counting game. Draw an imaginary line from any point to the edge of your element. Every time that line crosses a path edge, count it. If you end up with an odd number, that area gets filled. Even number? It stays transparent. This is why star centers appear hollow, the crossing lines create even-numbered intersections there.
  • Nonzero rule: (default value, on the right) This one’s about direction and flow. As your path travels around the shape, it creates a “winding” effect. Areas that get wound in one direction stay filled, while areas where clockwise and counter-clockwise paths cancel each other out become transparent. In most simple shapes like our star, everything winds the same way, so everything stays filled.

This gives you precise control over complex self-intersecting shapes, letting you create intricate patterns with internal cutouts or solid fills, all depending on which fill rule you choose.

Wrapping up

We’ve covered a lot of ground here. From simple circles to complex self-intersecting stars, clip-path gives you an entirely new vocabulary for shaping your interface. We started with basic geometry, built up to custom polygons, and finally broke free from straight lines with curves and precision.

But here’s the thing: everything we’ve explored so far has been about containment. About cutting away, hiding, cropping. We’ve been thinking inside the box, even when we’re changing its shape.

What if I told you there’s another way to think about paths in CSS? What if, instead of using them to constrain and contain, you could use them to guide and direct? What if your elements could follow curves, travel along custom routes, and move through space in ways that feel natural and intentional?

That’s exactly where we’re heading in part two. We’re going to shift from static shapes to dynamic motion, from clip-path to offset-path. Your elements won’t just be differently shaped—they’ll be dancing along curves you design, following trajectories that bring your interface to life.

The path of least resistance is about to get a whole lot more interesting.

Article Series

]]>
https://frontendmasters.com/blog/the-path-of-least-resistance-part-1/feed/ 0 6758
SVG to shape() https://frontendmasters.com/blog/svg-to-shape/ https://frontendmasters.com/blog/svg-to-shape/#respond Wed, 04 Jun 2025 11:23:09 +0000 https://frontendmasters.com/blog/?p=6042 in SVG ported to CSS so it can use actual units. It’s probably how path() should have ported to begin with, but c’est la vie. I’ll make the point in this demo. Resize those […]]]> We’ve been trying to make the point around here that the new shape() in CSS is awesome. It’s the powerful <path> in SVG ported to CSS so it can use actual units. It’s probably how path() should have ported to begin with, but c’est la vie.

I’ll make the point in this demo. Resize those containers and see how the clip-path responds (path() cannot be fluid, shape() can).

Are SVG <path>s 1-to-1 convertible to shape(), though? Apparently they are! (!!). Two people have already built converters and from what I’ve tested they work great.

]]>
https://frontendmasters.com/blog/svg-to-shape/feed/ 0 6042
Move Modal in on a… shape() https://frontendmasters.com/blog/move-modal-in-on-a-shape/ https://frontendmasters.com/blog/move-modal-in-on-a-shape/#comments Thu, 22 May 2025 18:27:26 +0000 https://frontendmasters.com/blog/?p=5917 , as we can do both open & close animations now.]]> Years ago I did a demo where a modal was triggered open and it came flying in on a curved path. I always thought that was kinda cool. Time has chugged on, and I thought I’d revisit that with a variety of improved web platform technology.

  1. Instead of a <div> it’ll be a proper <dialog>.
  2. We’ll set it up to work with no JavaScript at all. But we’ll fall back to using the JavaScript methods .showModal() and .close() to support browsers that don’t support the invoker command stuff.
  3. We’ll use @starting-style, which is arguably more verbose, but allows for opening and closing animations while allowing the <dialog> to be display: none; when closed which is better than it was before where the dialog was always in the accessibility tree.
  4. Instead of path() for the offset-path, which forced us into pixels, we’ll use shape() which allows us to use the viewport better. But we’ll still fall back to path().
  5. We’ll continue accounting for prefers-reduced-motion however we need to.

Here’s where the refactor ends up:

1. Use a Dialog

The <dialog> element is the correct semantic choice for this kind of UI, generally. But particularly if you are wanting to force the user to interact with the dialog before doing anything else (i.e. a “modal”) then <dialog> is particularly good as it moves then traps focus within the dialog.

2. Progressively Enhanced Dialog Open and Close

I only just learned you can open a modal (in the proper “modal” state) without any JavaScript using invokers.

So you can do an “open” button like this, where command is the literal command you have to call to open the modal and the commandfor matches the id of the dialog.

<button
  command="show-modal"
  commandfor="my-dialog"
>
  Open Modal
</button>

You may want to include popovertarget="my-dialog" as well, which is a still-no-JS fallback that will open the modal in a non-modal state (no focus trap) in browsers that don’t support invokers yet. Buttttttttt, we’re going to need a JavaScript fallback anyway, so let’s skip it.

Here’s how a close button can be:

<button
  command="close"
  commandfor="my-dialog"
>
  Close
</button>

For browsers that don’t support that, we’ll use the <dialog> element’s JavaScript API to do the job instead (use whatever selectors you need):

// For browsers that don't support the command/invokes/popup anything yet.
if (document.createElement("button").commandForElement === undefined) {
  const dialog = document.querySelector("#my-dialog");
  const openButton = document.querySelector("#open-button");
  const closeButton = document.querySelector("#close-button");

  openButton.addEventListener("click", () => {
    dialog.showModal();
  });

  closeButton.addEventListener("click", () => {
    dialog.close();
  });
}

At this point, we’ve got a proper dialog that opens and closes.

3. Open & Close Animation while still using display: none;

One thing about <dialog> is that when it’s not open, it’s display: none; automatically, without you having to add any additional styles to do that. Then when you open it (via invoker, method, or adding an open attribute), it becomes display: block; automatically.

For the past forever in CSS, it hasn’t been possible to run animations on elements between display: none and other display values. The element instantly disappears, so when would that animation happen anyway? Well now you can. If you transition the display property and use the allow-discrete keyword, it will ensure that property “flips” when appropriate. That is, it will immediately appear when transitioning away from being hidden and delay flipping until the end of the transition when transitioning into being hidden.

dialog {
  transition: display 1.1s allow-discrete;
}

But we’ll be adding to that transition, which is fine! For instance, to animate opacity on the way both in and out, we can do it like this:

dialog {
  transition:
    display 1.1s allow-discrete,
    opacity 1.1s ease-out;
  opacity: 0;

  &[open] {
    opacity: 1;
    @starting-style {
      opacity: 0;
    }
  }
}

I find that kinda awkward and repetitive, but that’s what it takes and the effect is worth it.

4. Using shape() for the movement

The cool curved movement in the original movement was thanks to animating along an offset-path. But I used offset-path: path() which was the only practical thing available at the time. Now, path() is all but replaced by the way-better-for-CSS shape() function. There is no way with path() to express something like “animate from the top left corner of the window to the middle”, because path() deals in pixels which just can’t know how to do that on an arbitrary screen.

I’ll leave the path() stuff in the to accommodate browsers not supporting shape() yet, so it’ll end up like:

dialog {
  ...

  @supports (offset-rotate: 0deg) {
    offset-rotate: 0deg;
    offset-path: path("M 250,100 S -300,500 -700,-200");
  }
  @supports (
    offset-path: shape(from top left, curve to 50% 50% with 25% 100%)
  ) {
    offset-path: shape(from top left, curve to 50% 50% with 25% 100%);
    offset-distance: 0;
  }
}

That shape() syntax expresses this movement:

Those points flex to whatever is going on in the viewport, unlike the pixel values in path(). Fun!

This stuff is so new from a browser support perspective, I’m finding that Chrome 126, which is the stable version as I write, does support clip-path: shape(), but doesn’t support offset-path: shape(). Chrome Canary is at 128, and does support offset-path: shape(). But the demo is coded such that it falls back to the original path() by using @supports tests.

Here’s a video of it working responsively:

5. Preferring Less Motion

I think this is kind of a good example of honoring the intention.

@media (prefers-reduced-motion) {
  offset-path: none;
  transition: display 0.25s allow-discrete, opacity 0.25s ease-out;
}

With that, there is far less movement. But you still see the modal fade in (a bit quicker) which still might be a helpful animation emphasizing “this is leaving” or “this is entering”.

]]>
https://frontendmasters.com/blog/move-modal-in-on-a-shape/feed/ 1 5917
Creating Blob Shapes using clip-path: shape() https://frontendmasters.com/blog/creating-blob-shapes-using-clip-path-shape/ https://frontendmasters.com/blog/creating-blob-shapes-using-clip-path-shape/#respond Mon, 19 May 2025 14:30:05 +0000 https://frontendmasters.com/blog/?p=5861 After the flower shapes, let’s move to one of the coolest shapes: the Blob! Those distorted wiggly circles that were almost impossible to achieve using CSS. But now, they are possible using the new shape() function.

Article Series

Before we start, take a look at my blob shape generator. Unlike the flower shapes, blobs have the random factor so having a generator to get the code is a lifesaver. This said, stay with me to understand the logic behind creating them, maybe you will want to make your own generator of blobs.

For this shape, we are going to rely on the curve command, so let’s start by understanding how it works.

At the time of writing, only Chrome, Edge, and Safari have the full support of the features we will be using.

The curve Command

This command allows you to draw Bézier curves between two points. With the arc command, we needed a radius, but here we need control points. We can either have one control point and create a Quadratic curve or two control points and create a Cubic curve.

Here is a figure to illustrate a few examples. The black dots illustrate the control points, and the blue ones the starting and ending points.

Illustration showing the difference between creating curves with one control point and two control points using Bézier curves. The top left and bottom left demonstrate the one control point method, while the top right and bottom right illustrate the two control points method.

And a demo:

I won’t detail the exact geometry behind the curves, but notice their behavior close to the starting and ending points. The curve is tangent to the lines that link the starting and ending points with the control points. This will be the key to create our blob shape.

The code of this command is:

clip-path: shape(from Xa Ya, curve to Xb Yb with Xc1 Yc1 / Xc2 Yc2) 

And I will be using one control point, so we can omit the second control point:

clip-path: shape(from Xa Ya, curve to Xb Yb with Xc1 Yc1) 

By combining many curves, we can create a blob. We have to understand how to correctly combine them, so it’s time for a small geometry course.

The Geometry of The Blob

Mathematically speaking, there is no specific geometry for a blob because it’s not a shape we can formally define. We can implement a blob using different methods and calculations, so what I am going to share is my own implementation. It’s probably not the best one, but it gives a good result.

We first start by placing N points around a circle. The number of points is the first parameter of the blob that I am calling “granularity” in my generator. Then I define a distance D I call the depth.

Now, we randomly move the points within the area defined by the distance D. For each point, we pick a random value in the range [0 D] and we make it closer to the center using that value.

For the next step, we take two consecutive points, draw a line between them, and then place a new point at the center. This will double the number of points.

The last step is to draw the Bézier curves. The new points (the blue ones) are the starting and ending points, and the initial points (the black ones) are the control points.

The fact that two adjacent curves share the same tangent is what gives us a continuous and smooth shape, a perfect blob!

That’s it. Now let’s translate this into code.

The Code of The Blob

Similar to the flower shape, the code will be a bunch of curve commands like below:

.blob { 
  clip-path: shape(from X0 Y0, 
     curve to X1 Y1 with Xc1 Yc1,
     curve to X2 Y2 with Xc2 Yc2,
     curve to X3 Y3 with Xc3 Yc3,
     curve to X4 Y4 with Xc4 Yc4,
     ...
     curve to Xn Yn with Ycn Ycn
  )
}

[Xi, Yi] are the starting/ending points (the blue ones), and [Xci, Yci] are the control points (the black ones). For the sake of simplicity, I will use pseudo-code to illustrate the calculation. The real implementation can be done using JavaScript like in my generator, or using Sass (I will share a demo using Sass later).

We first start by defining the control points:

N = 15  /* number of points (granularity) */
D = 20% /* depth */

for i in [1 N] {
R = 50% - random(D);
Xci = 50% + R*math.cos(360deg * i/N));
Yci = 50% + R*math.sin(360deg * i/N));
}

R will define the distance of the points from the center, and it will have a random value between 50% and 50% - D.

Then we define the main points where each one is placed at the center of two consecutive control points:

for i in [1 N] {
Xi = (Xci + Xci+1)/2;
Yi = (Yci + Yci+1)/2;
}

Finally, the shape function will be as follows:

clip-path: shape(from X0 Y0, 
for i in [1 N] {
curve to Xi Yi with Xci Yci,
}
)

Here is a Sass implementation:

One observation we can make is that the shape is responsive. It’s designed to work with square elements (aspect-ratio: 1), but the result is not bad for rectangular elements as well. Resize the element in the demo below and see how the shape behaves:

The code can also be tweaked to create more variations. We can, for example, have a kind of wavy circle by removing the random part and applying a fixed distance to half the points.

Can you think of other variations?

Animating The Blob

Having the blob shape in CSS is already a cool feature. It’s one line of code that can be applied to any element, including images, and it’s responsive! In addition to this, we can easily animate them. The only requirement is to have the same structure inside shape(). So if we take two blobs having the same number of curve commands, then we can animate one into another!

Here is an example where we keep the same number of points and only adjust the depth:

You copy both codes from the generator, apply a transition, and you have a cool hover effect that transforms a circle into a blob!

The bouncing effect you get is made with the linear() function which is another cool feature for custom easing. I am getting the code from here.

Now, if you update the Shape ID and still keep the same number of points, you can have a transition between two different blobs.

Cool, right? The code may look complex but in the end everything is generated for you, so it’s nothing but a few clicks to get a fancy shape with a nice animation! Speaking about animation, let’s end with a demo using a keyframes instead of a transition.

Conclusion

I hope you enjoyed this shape() exploration through this series of articles. Once this feature becomes widely supported, it will be a game changer and we can forget about all the hacky workarounds to create CSS shapes.

Don’t forget to keep an eye on my CSS Shapes and CSS Generators websites from where you can easily copy the code of any CSS shape.

Article Series

]]>
https://frontendmasters.com/blog/creating-blob-shapes-using-clip-path-shape/feed/ 0 5861
Creating Flower Shapes using clip-path: shape() https://frontendmasters.com/blog/creating-flower-shapes-using-clip-path-shape/ https://frontendmasters.com/blog/creating-flower-shapes-using-clip-path-shape/#respond Mon, 12 May 2025 14:51:36 +0000 https://frontendmasters.com/blog/?p=5824 In a previous article, we used modern CSS features such as mask, trigonometric functions, and CSS variables to create flower-like shapes.

The HTML code was a single element, which means we can apply the CSS to image elements and get cool frames like the demo below:

In this article, we are redoing the same shapes using the new shape() function, which I think will become my favorite CSS feature.

At the time of writing, only Chrome, Edge, and Safari have the full support of the features we will be using.

Article Series

What is shape()?

You are probably familiar with clip-path: polygon(), right? A function that allows you to specify different points, draw straight lines between them and create various CSS shapes (I invite you to check my online collection of CSS shapes to see some of them). I said “straight lines” because when it comes to curves, clip-path is very limited. We have circle() and ellipse(), but we cannot achieve complex shapes with them.

shape() is the new value that overcomes such limitation. In addition to straight lines, it allows us to draw curves. But If you check the MDN page or the specification, you can see that the syntax is a bit complex and not easy to grasp. It’s very similar to SVG path, which is good as it gives us a lot of options and flexibility, but it requires a lot of practice to get used to it.

I will not write a boring tutorial explaining the syntax and all the possible values, but rather focus on one command per article. In this article, we will study the arc command, and the next article will be about the curve command. And, of course, we are going to draw cool shapes. Otherwise it’s no fun!

The arc Command

This command allows you to draw an elliptic arc between two points but I will only consider the particular case of a circle which is easier and the one you will need the most. Let’s start with some geometry to understand how it works.

If you have two points (A and B) and a radius, there are exactly two circles with the given radius that intersect with the points. The intersection of both circles gives us 4 possible arcs between A and B that we can identify with a size (small or large) and a direction (clockwise or counter-clockwise)

The code will look like the below:

clip-path: shape(from Xa Ya, arc to Xb Yb of R [large or small] [cw or ccw])

The first command of a shape is always a from to give the starting point, and then we use the arc to define the ending point, the radius, the size, and the direction.

Here is a demo to illustrate the different values:

The points and the radii are the same. I am only changing the size and direction to choose one of the four possibilities. It should be noted that the size and direction aren’t mandatory. The defaults are small and ccw.

That’s all: we have what we need to draw flower shapes!

Creating The Flower Shape

I will start with a figure from the previous article.

Using the mask method, we had to draw a big circle at the center and small circles placed around it. Using shape(), we need to draw the arcs of the small circles. The starting and ending points of each arc are placed where the circles touch each other.

The code will look as follows:

.flower { 
  clip-path: shape(from X0 Y0, 
     arc to X1 Y1 of R,
     arc to X2 Y2 of R,
     arc to X3 Y3 of R,
     arc to X4 Y4 of R,
     ...
     arc to Xn Yn of R
  )
}

And like I did with the mask method, I will rely on Sass to create a loop.

$n: 10;

.flower {
  $m: ();
  $m: append($m,from X0 Y0,comma);
  @for $i from 1 through $n {
    $m: append($m,arc to Xi Yi of R,comma);
  } 
  clip-path: shape(#{$m});
}

Now we need to identify the radius of the small circles (R) and the position of the different points (Xi, Yi). I already did the calculation of the radius in the previous article, so I will reuse the same value:

R = 50%/(1 + 1/math.sin(180deg/$n))

For the points, they are placed around the same circle so their coordinate can be written like below:

Xi = 50% + D*math.cos($i*360deg/$n)
Yi = 50% + D*math.sin($i*360deg/$n)

Here is a figure to illustrate the distance D and the radius R:

I know you don’t want a boring geometry course so here is the value of D.

D = 50%/(math.tan(180deg/$n) + 1/math.cos(180deg/$n))

And the final code will be:

$n: 10;

.flower {
  $m: ();
  $r: 50%/(1 + 1/math.sin(180deg/$n));
  $d: 50%/(math.tan(180deg/$n) + 1/math.cos(180deg/$n));
  $m: append($m,from 
    50% + $d*math.cos(0) 
    50% + $d*math.sin(0),comma);
  @for $i from 1 through $n {
    $m: append($m,arc to 
      50% + $d*math.cos($i*360deg/$n)
      50% + $d*math.sin($i*360deg/$n)
      of $r,comma);
  } 
  clip-path: shape(#{$m});  
}

Wait, we get the inverted shape instead? Why is that?

We didn’t define the size and the direction of the arcs so by default the browser will use small and ccw. This gives us the inverted version of the flower. If you try the 4 different combinations (including the default one) you will get the following:

small ccw and large cw give us the shape we are looking for. The small cw is an interesting variation, and the large ccw is a funny one. We can consider a CSS variable to easily control which shape we want to get.

Combining Both Shapes

Now, what about the shape below?

The idea is to use the same code and alternate between two arc configurations.

$n: 10;

.flower {
  $m: ();
  $r: 50%/(1 + 1/math.sin(180deg/$n));
  $d: 50%/(math.tan(180deg/$n) + 1/math.cos(180deg/$n));
  $m: append($m,from 
    50% + $d*math.cos(0) 
    50% + $d*math.sin(0),comma);
  @for $i from 1 through $n {
    $c:small ccw;
    @if $i % 2 == 0 {$c:large cw}
    $m: append($m,arc to 
      50% + $d*math.cos($i*360deg/$n)
      50% + $d*math.sin($i*360deg/$n)
      of $r $c,comma);
  } 
  clip-path: shape(#{$m});  
}

I introduced a new variable $c within the loop that will have the value small ccw when $i is odd and large cw otherwise.

Cool right? The compiled code will look like the below:

clip-path: 
shape(from X0 Y0,
arc to X1 Y1 of R small ccw,
arc to X2 Y2 of R large cw,
arc to X3 Y3 of R small ccw,
arc to X4 Y4 of R large cw,
...
)

The first arc will use the inner curve (small ccw), the next one the outer curve (large cw) and so on. We repeat this to get our wavy-flower shape!

Optimizing The Code

We made a generic code that allow us to get any shape variation by simply controlling the size/direction of the arcs, but for each particular case, we can have a more simplified code.

For the inverted variation (small ccw), the value of D can be replaced by 50%. This will simplify the formula and also increase the area covered by the shape. We also need to update the value of the radius.

$n: 10;

.flower {
  $m: ();
  $r: 50%*math.tan(180deg/$n);
  $m: append($m,from 
    50% + 50%*math.cos(0) 
    50% + 50%*math.sin(0),comma);
  @for $i from 1 through $n {
    $m: append($m,arc to 
      50% + 50%*math.cos($i*360deg/$n)
      50% + 50%*math.sin($i*360deg/$n)
      of $r,comma);
  } 
  clip-path: shape(#{$m});  
}

We can do the same for the main shape, but this time we can simplify the value of the radius and use 1%.

$n: 10;

.flower {
  $m: ();
  $d: 50%/(math.cos(180deg/$n)*(1 + math.tan(180deg/$n)));
  $m: append($m,from 
    50% + $d*math.cos(0) 
    50% + $d*math.sin(0),comma);
  @for $i from 1 through $n {
    $m: append($m,arc to 
      50% + $d*math.cos($i*360deg/$n)
      50% + $d*math.sin($i*360deg/$n)
      of 1% cw,comma);
  } 
  clip-path: shape(#{$m});  
}

This one is interesting because using 1% as a radius is kind of strange and not intuitive. In the explanation of the arc command, I said that we have exactly two circles with the given radius that intersect with the two points, but what if the radius is smaller than half the distance between the points? No circles can meet that condition.

This case falls into an error handling where the browser will scale the radius until we can have at least one circle that meets the conditions (yes, it’s defined within the specification). That circle will simply have its radius equal to half the distance between both points. It also means we only have two arcs with the same size (small and large will be equal)

In other words, if you specify a small radius (like 1%), you are telling the browser to find the radius value for you (a lazy but clever move!). This trick won’t work in all the situations but can be handy in many of them so don’t forget about it.

Conclusion

We are done with this first article! You should have a good overview of the arc command and how to use it. I only studied the particular case of drawing circular arcs but once you get used to this you can move to the elliptic ones even if I think you will rarely need them.

Let’s end with a last demo of a heart shape, where I am using the arc command. Can you figure out how to do it before checking my code?

And don’t forget to bookmark my online generators from where you can get the code of the flower shapes and more!

Article Series

]]>
https://frontendmasters.com/blog/creating-flower-shapes-using-clip-path-shape/feed/ 0 5824
shape(): A New Powerful Drawing Syntax in CSS https://frontendmasters.com/blog/shape-a-new-powerful-drawing-syntax-in-css/ https://frontendmasters.com/blog/shape-a-new-powerful-drawing-syntax-in-css/#comments Wed, 07 May 2025 14:20:49 +0000 https://frontendmasters.com/blog/?p=5662 that we absolutely needed.]]> I first saw in the Safari 18.4 release notes that shape(), a new function is now supported. Then I saw on MDN it’s actually already in Chrome, too!

The shape() function joins friends like polygon(), circle(), rect(), inset(), and a handful of others. These functions are used as values for a handful of things in CSS, namely:

  • clip-path — Clipping away parts of elements
  • offset-path — Moving elements along a path
  • shape-outside — Applied to a float-ed element such that content flows along the path

Fair warning: shape() only seems to work with clip-path. I couldn’t find a ton of information on this, but the Chrome blog does state it. It will probably work with the other properties in due time.

Let’s focus on clip-path here which I might argue is the most useful anyway, as it makes an entire element into the shape described which feels like a more generally applicable thing.

I got into this on the CodePen blog where I equated shape() to <path d=""> in SVG, which is surely the intention. You can actually set the d attribute in CSS, but it only works on <path> elements, and the unitless values translate only to pixels, which makes it not particularly CSSy or useful.

One situation I mentioned was Trys Mudford’s blog post where this was the design situation at hand:

Oh look, a use case.

Those light yellow boxes are basically polygons with rounded corners. In a perfect world, polygon() could do this with the round keyword, as specced, but alas that doesn’t work just yet. But because shape() is essentially all-powerful, that does work now (in Chrome and Safari anyway, and this feels like a decently progressive-enhancement thing).

Temani Afif saw that and did the work!

This is very awesome. This is quite the power tool for shape-making. I think we’re going to see a lot of fancy stuff come out of this.

It’s true we already have a path() function, but remember, it’s sooooo limited. The values are only pixels, which are some pretty big handcuffs in a responsive world full of intrinsic content (that is, elements on the web that respond to their contents and environment). Simon Fraser on the WebKit blog introduces this new feature and calls it out:

… using path() in clip-path can’t be responsive; you can’t write CSS rules so that the path adapts to the size of the element. This is where the new shape() function comes in.

Coincidentally, Simon’s demo (Jen’s demo?) also shows off an arrow shape:

That’s using multiple different drawing commands (line and arc, but there are more), keywords like top and left (excellent, but I wonder why logical properties don’t work?), and, even more deliciously, container units (e.g. cqh). The orange border there is a good reminder that clip-path, well, clips. So it’ll lop off anything at all on this element in those areas, including content.

Noam Rosenthal got in on the fun over on the Chrome for Developers blog, underscoring just how hard this stuff used to be:

clip-path: shape() lets you clip your element using arbitrary and responsive shapes, previously only possible using techniques like conic gradients or JavaScript-constructed SVG.

And like all this good company, absolutely couldn’t resist peppering in other CSS goodness into a demo. His demo here uses different drawing commands than we’ve seen so far, custom properties (which are an extremely natural fit), and even animation (!!).

I see Temani is hard on the case with a blob generator using shape(), which, I believe as long as there are the “same number of points”, can be animated by changing the clip-path entirely. Like:

And obviously I love this:

The Actual Shape Commands

The spec covers them, but the best writeup I’ve seen is Geoff’s on CSS-Tricks. He’s got a bit more detail so check that out, but here’s the list:

  • line
  • vline
  • hline
  • arc
  • curve
  • smooth

Each of them have a bit of sub-syntax to themselves. Like the curve command might look like curve to 50% 50% with 50% 0 which would continue drawing the shape to the exact center of the element in a curve in which the top center is a “control point” and so curves in that direction.

In my experience it’s quite easy to make a small mistake in the syntax and wreck the whole thing. But hey that’s understandable.

Squircles with shape()

I get to have some fun too! It occurred to me that digital designs most elusive beast, the squircle, might be now achievable with reasonable normal web tech.

SVG can do it, but I wouldn’t call it particularly readable code. “Monoco is a tiny JavaScript library that adds squircles” (via SVG background images) and it does a pretty good job of it I’d say, but that’s more technology than I normally like to throw at something like this. Jared White by way of Simeon Griggs has a pretty nice SVG-based solution as well, leveraging SVG-as-clip-path.

I like how relatively chill that SVG path is, but still, shape() can allow us to squish this down into just CSS which is kinda sweet.

That is… if I was fully smart enough to do it.

I crudely drew one in Figma so that I could label the points for writing the syntax out.

I figured if I just did a curve to every one of those points with control points a bit the edges, it would… work? So basically like this:

div {
  clip-path: shape(
    from 5% 3%,
    curve to 95% 3% with 50% 0,
    curve to 97% 5% with 97% 3%,
    curve to 97% 95% with 100% 50%,
    curve to 95% 97% with 97% 97%,
    curve to 5% 97% with 50% 100%,
    curve to 3% 95% with 3% 97%,
    curve to 3% 5% with 0% 50%,
    curve to 5% 3% with 3% 3%,
  );
}

Which basically works. I tried playing around with arc and smooth instead but couldn’t manage to make it any better (with my like zero geometry skills). Then instead of hard coding those percentage values, I made them in custom properties with sliders to squiggle them around a little.

It’s a little janky — but I trust someone make like a real quality geometrically sound version eventually.


Update #1

I heard from Peter Herbert over email:

I found a somewhat more accurate version of the iOS squircle. Apparently the Apple squircle uses three cubic beziers in each corner. The original research that figured out the curves I found here, and I used Claude to find the points.

Update #2

Matthew Morete commented below with a tool he made that converts SVG path commands into shape() commands, which is awesome. Squircles are one of the provided demos, and the commands are very chill:

.squircle {
  clip-path: shape(
    from 0% 50%, 
    curve by 50% -50% with 0% -45% / 5% -50%, 
    smooth by 50% 50% with 50% 5%, 
    smooth by -50% 50% with -5% 50%, 
    smooth by -50% -50% with -50% -5%
  );
}
]]>
https://frontendmasters.com/blog/shape-a-new-powerful-drawing-syntax-in-css/feed/ 7 5662