Clip-path, Shapes & Masks

CSS · Lesson 40 of 55 8 min read Updated

Every element on a web page is a rectangle. Most of the time that is fine, but designs often call for something else: a circular profile photo, a hero section with a slanted bottom edge, a hexagon badge, text that curves around a round picture, or a long paragraph that fades out softly before a "Read more" link.

In this lesson you will cut elements into shapes with clip-path, animate those shapes, make text follow a curve with shape-outside, and create soft fades with CSS masks.

clip-path: cut an element to a shape

clip-path hides every part of an element that falls outside a shape. Think of it as cutting a photo with scissors: the box still takes the same space in the layout, but only the inside of the shape is painted. The cut-off parts also stop responding to clicks and hovers.

The basic shapes are functions:

FunctionShapeExample
inset()A smaller rectangle, optionally roundedinset(10px round 16px)
circle()A circle with a radius and a centrecircle(40% at 50% 50%)
ellipse()An oval with two radiiellipse(50% 35%)
polygon()Any straight-edged shape, point by pointpolygon(50% 0, 100% 100%, 0 100%)
path()An SVG path string, for curvespath("M 0 0 L 100 0 ...")

Points in polygon() are written as x y pairs, measured from the top-left corner of the element, and separated by commas. Percentages make the shape scale with the element, which is why they are usually the best choice.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>clip-path shapes</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 1rem; }
    .shapes { display: grid; grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); gap: 1rem; }
    .shapes figure { margin: 0; text-align: center; font-size: 0.85rem; }
    .shapes img { width: 100%; height: auto; aspect-ratio: 1; object-fit: cover; display: block; }
    .inset    { clip-path: inset(8% round 20px); }
    .circle   { clip-path: circle(45%); }
    .ellipse  { clip-path: ellipse(50% 32%); }
    .triangle { clip-path: polygon(50% 0, 100% 100%, 0 100%); }
    .hexagon  { clip-path: polygon(25% 5%, 75% 5%, 100% 50%, 75% 95%, 25% 95%, 0 50%); }
    .hero {
      margin-top: 1.5rem;
      padding: 2rem 1.5rem 4rem;
      color: white;
      background: linear-gradient(135deg, #1e3a8a, #7c3aed);
      clip-path: polygon(0 0, 100% 0, 100% 75%, 0 100%);   /* slanted bottom edge */
    }
  </style>
</head>
<body>
  <div class="shapes">
    <figure><img class="inset" src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Hills and a river at sunrise" width="800" height="500">inset()</figure>
    <figure><img class="circle" src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Hills and a river at sunrise" width="800" height="500">circle()</figure>
    <figure><img class="ellipse" src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Hills and a river at sunrise" width="800" height="500">ellipse()</figure>
    <figure><img class="triangle" src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Hills and a river at sunrise" width="800" height="500">triangle</figure>
    <figure><img class="hexagon" src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Hills and a river at sunrise" width="800" height="500">hexagon</figure>
  </div>
  <section class="hero">
    <h1>Kochi Coding Club</h1>
    <p>Free weekend workshops for college students.</p>
  </section>
</body>
</html>

All five pictures use the same file; only the clip changes. The triangle has three points: top centre, bottom right and bottom left. The hexagon has six. The purple section shows a common layout use: its bottom edge runs from 100% down at the left to 75% at the right, giving a slanted edge without any image. Try changing 75% to 60%.

Animating clip-path

The browser can animate between two shapes of the same type: two circle() values, two inset() values, or two polygon() values with the same number of points. That makes neat reveal effects.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>clip-path reveal</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 1.5rem; }
    .reveal { position: relative; max-width: 20rem; border-radius: 12px; overflow: hidden; }
    .reveal img { width: 100%; height: auto; display: block; }
    .reveal .offer {
      position: absolute;
      inset: 0;
      display: grid;
      place-content: center;
      text-align: center;
      background: rgb(15 23 42 / 80%);
      color: white;
      clip-path: circle(0% at 50% 50%);          /* hidden: a circle with no size */
      transition: clip-path 0.5s ease;
    }
    .reveal .offer a { color: #fde68a; }
    .reveal:hover .offer,
    .reveal:focus-within .offer {
      clip-path: circle(75% at 50% 50%);         /* big enough to cover the corners */
    }
    @media (prefers-reduced-motion: reduce) {
      .reveal .offer { transition: none; }
    }
  </style>
</head>
<body>
  <div class="reveal">
    <img src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Hills and a river at sunrise near Munnar" width="800" height="500">
    <div class="offer">
      <p><strong>Monsoon offer</strong><br>3 nights in Munnar from ₹7,999</p>
      <a href="#book">Book now</a>
    </div>
  </div>
</body>
</html>

Point at the picture, or tab to the link: the dark panel grows from the centre as an expanding circle. Both states use circle(), so the browser can blend between them. A radius of 75% is enough to cover the corners of the box.

Wrapping text around shapes with shape-outside

clip-path changes how an element looks, but text around a floated image still wraps around its rectangular box. shape-outside changes the shape that text flows around. It only works on floated elements (see Float & Clear), and takes the same basic shapes as clip-path.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>shape-outside</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 1rem; }
    article { max-width: 34rem; line-height: 1.6; }
    .round {
      float: left;
      width: 11rem;
      height: auto;
      aspect-ratio: 1;
      object-fit: cover;
      clip-path: circle(50%);       /* what you see */
      shape-outside: circle(50%);   /* what the text wraps around */
      shape-margin: 1rem;           /* gap between the curve and the text */
    }
  </style>
</head>
<body>
  <article>
    <img class="round" src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Hills and a river at sunrise" width="800" height="500">
    <p>Isha grew up in a small town in the hills, where the river below the house turned gold every morning at sunrise. Her grandmother would wake her early so they could walk down to the water before the tea gardens got busy.</p>
    <p>Years later, as a photographer in Pune, she still wakes before dawn. She says the first ten minutes of sunlight are the only time the hills look exactly the way she remembers them from childhood.</p>
  </article>
</body>
</html>

The image is clipped to a circle, and the lines of text curve along the same circle, keeping a 1rem gap set by shape-margin. Remove the shape-outside line to see the text go back to a straight edge.

shape-outside can also take an image URL. The text then wraps around the non-transparent pixels of that image. The image must come from the same website, or from a server that allows cross-origin use.

Soft edges with CSS masks

clip-path cuts with hard edges. A mask uses an image as a stencil instead, and it can be soft. With the default settings, wherever the mask image is opaque the element shows, wherever it is transparent the element is hidden, and half-transparent parts show the element half-faded. Gradients make perfect masks, because they need no file.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>CSS masks</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 1rem; display: flex; flex-wrap: wrap; gap: 2rem; align-items: start; }
    .fade-text {
      max-width: 18rem;
      max-height: 7.5rem;
      overflow: hidden;
      line-height: 1.5;
      -webkit-mask-image: linear-gradient(black 50%, transparent);
      mask-image: linear-gradient(black 50%, transparent);
    }
    .vignette {
      width: 18rem;
      height: auto;
      -webkit-mask-image: radial-gradient(ellipse 50% 50%, black 60%, transparent);
      mask-image: radial-gradient(ellipse 50% 50%, black 60%, transparent);
    }
  </style>
</head>
<body>
  <div>
    <p class="fade-text">Rohan's review of the new library in Lucknow: the reading hall is bright and quiet, the Wi-Fi works well, and the staff helped him find old exam papers for his engineering entrance practice. He plans to study there every weekend until the exams.</p>
    <a href="#review">Read more</a>
  </div>
  <img class="vignette" src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Hills and a river at sunrise" width="800" height="500">
</body>
</html>

The paragraph is fully visible at the top and fades to nothing at the bottom, a cleaner end than a hard cut. The photo uses a radial gradient as its mask, so its edges melt into the page like an old photograph.

Masks work much like backgrounds, with matching properties: mask-size, mask-position, mask-repeat and the mask shorthand. mask-mode chooses whether the mask uses transparency (alpha, normal for images and gradients) or brightness (luminance). A mask image from a URL, such as an SVG shape, must come from the same website or allow cross-origin use. You will see masks used to colour icons in Icons.

The unprefixed mask-image property works in current versions of all major browsers. Chrome, Edge and Safari only added it in 2022 and 2023, and older versions needed -webkit-mask-image, so many stylesheets write both lines, as the example does.

Clip or mask?

clip-pathmask-image
EdgesHardHard or soft
Defined byA shape functionAn image or gradient
AnimateBetween shapes of the same typeThrough mask-position or mask-size
Good forCircles, polygons, slanted sections, revealsFades, vignettes, icon shapes

Both create a new stacking context, which affects layering (see Z-index & Stacking Contexts).

Common mistakes

  • Polygon points in the wrong order. Go around the shape in one direction, or the edges cross.
  • Expecting the layout to change. A clipped element still takes its full rectangular space.
  • Animating between different shape types. circle() to polygon() jumps instead of animating.
  • shape-outside on an element that is not floated. It only affects floats.
  • Forgetting the unprefixed mask-image or the prefixed one. Write both while you need to support older browsers.
  • Hiding text people need. Clipped and masked text is invisible but still read by screen readers; make sure the full content is available.

Key points

  • clip-path shows only the part of an element inside a shape: inset(), circle(), ellipse(), polygon(), path() and the new shape().
  • Polygon points are x y pairs from the top-left corner; percentages make shapes scale.
  • Shapes of the same type can be transitioned for reveal effects.
  • shape-outside makes text wrap around a shape, but only around floats; shape-margin adds a gap.
  • mask-image uses an image or gradient as a soft stencil, ideal for fades and vignettes.
  • Clipped areas do not take clicks, and both clips and masks create stacking contexts.

Exercise

Design the top of a page for a Jaipur kite festival:

  • a header with a gradient background and a slanted bottom edge made with clip-path;
  • inside it, the landscape image clipped to a hexagon;
  • below the header, a paragraph about the festival that fades out at the bottom with a gradient mask, followed by a "Read more" link.
Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Kite festival</title>
  <style>
    body { font-family: system-ui, sans-serif; margin: 0; }
    header {
      display: flex;
      align-items: center;
      gap: 1.5rem;
      padding: 2rem 1.5rem 4rem;
      color: white;
      background: linear-gradient(120deg, #be185d, #f97316);
      clip-path: polygon(0 0, 100% 0, 100% 80%, 0 100%);
    }
    header img {
      width: 8rem;
      height: auto;
      aspect-ratio: 1;
      object-fit: cover;
      clip-path: polygon(25% 5%, 75% 5%, 100% 50%, 75% 95%, 25% 95%, 0 50%);
    }
    .intro {
      max-width: 34rem;
      max-height: 6rem;
      overflow: hidden;
      margin: 1rem 1.5rem 0;
      -webkit-mask-image: linear-gradient(black 40%, transparent);
      mask-image: linear-gradient(black 40%, transparent);
    }
    .more { margin-inline: 1.5rem; display: inline-block; }
  </style>
</head>
<body>
  <header>
    <img src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Hills and a river at sunrise" width="800" height="500">
    <h1>Jaipur Kite Festival</h1>
  </header>
  <p class="intro">Every January the sky over the old city fills with thousands of kites. Families gather on rooftops from early morning, food stalls sell til laddoos and pakoras, and the evening ends with paper lanterns rising into the dark.</p>
  <a class="more" href="#festival">Read more</a>
</body>
</html>

The header and the image both use polygon(): four points for the slanted header and six for the hexagon. The paragraph's mask is solid for the first 40% and fades to transparent at the bottom.

Quick Quiz

1. Where is the point 0 0 in polygon()?

2. A floated image is clipped to a circle, but the text still wraps in a straight line. What is missing?

3. Which can make a soft, faded edge?

4. Which transition will animate smoothly?

Learn CSS with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call