CSS Clip-Path Guide: Masking, Shapes, and Design Patterns

clip-path lets you cut an element down to a custom shape: a polygon, circle, ellipse, inset rectangle, or curve. If you need the full syntax for every shape function, our CSS clip-path generator covers it with a live visual editor and copy-ready output.

This guide picks up from there. It covers clip-path against its closest relative, mask-image, real design patterns you will actually reach for, making shapes responsive, converting design-tool exports into working CSS, using clip-path with Tailwind and React, and the debugging issues that trip people up once clip-path is in production.

clip-path vs mask-image: which one do you need

clip-path and mask-image both hide part of an element, but they work on completely different principles. clip-path defines a vector shape: everything inside the shape is shown, everything outside is gone, with a hard edge and no middle ground. mask-image uses an image or a gradient as a stencil, where the transparency of the mask controls the transparency of the element underneath, which means it can fade gradually instead of cutting sharply.

 clip-pathmask-image
Shape sourceVector shape functions or SVG pathImage, gradient, or SVG used as a stencil
TransparencyHard edge only, in or outSoft, partial transparency supported
Typical useGeometric crops, section dividers, reveal wipesFade-outs, spotlight effects, textured edges
Prefix neededNo, in every current browserOften yes, Safari still expects -webkit-mask-image

A gradient fade at the edge of an element is the clearest example of something clip-path cannot do at all, since every clip-path shape has a hard boundary:

.fade-edge { -webkit-mask-image: linear-gradient(to right, black 70%, transparent 100%); mask-image: linear-gradient(to right, black 70%, transparent 100%); }

mask-image also supports a mask-mode of luminance, where the brightness of each pixel in the mask controls opacity rather than its alpha channel, which is useful for spotlight-style reveals built from a plain white-to-transparent gradient:

.spotlight { mask-image: radial-gradient(circle at center, white 40%, transparent 75%); mask-mode: luminance; }

As a rule of thumb: if the edge in your design is a straight line, a circle, or a polygon, use clip-path. If the edge fades, blurs, or needs a gradient, use mask-image.

Real-world clip-path design patterns

Diagonal section divider that does not clip your content

Clipping a section directly is the most common first attempt at a diagonal divider, and the most common problem with it: the clip-path also cuts off any text or content inside the section, not just its background. The fix is to put the diagonal shape on a positioned pseudo-element behind the content instead of on the section itself, so only the background gets clipped.

.section-diagonal { position: relative; padding: 4rem 2rem; } .section-diagonal::before { content: ''; position: absolute; inset: 0; z-index: -1; background: linear-gradient(135deg, #7c6af7, #38bdf8); clip-path: polygon(0 0, 100% 8%, 100% 100%, 0 92%); }

The section's actual content, its headings and paragraphs, sits in the normal document flow above the pseudo-element and is never clipped.

Hexagonal image grid

A single hexagon is a five-minute polygon, but a grid of them for a portfolio or team page needs the shape combined with object-fit so every photo, regardless of its original aspect ratio, fills the hexagon cleanly.

.hex-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1.5rem; } .hex-grid img { width: 100%; aspect-ratio: 1 / 1.1; object-fit: cover; clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%); transition: transform 0.3s ease; } .hex-grid img:hover { transform: scale(1.06); }

Circular reveal wipe on hover

Transitioning clip-path directly, rather than using it as a static crop, is what makes a circle grow open from the center on hover, a common effect on image galleries and feature cards.

.reveal-circle { position: relative; overflow: hidden; } .reveal-circle img { clip-path: circle(0% at 50% 50%); transition: clip-path 0.5s ease; } .reveal-circle:hover img { clip-path: circle(75% at 50% 50%); }

Staggered card reveal with inset()

Animating inset() from fully covering an element to fully open creates a wipe reveal, and combining it with nth-child delays turns a plain grid of cards into a staggered entrance sequence.

@keyframes revealCard { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0% 0 0); } } .reveal-card { animation: revealCard 0.6s ease both; } .reveal-card:nth-child(2) { animation-delay: 0.1s; } .reveal-card:nth-child(3) { animation-delay: 0.2s; } .reveal-card:nth-child(4) { animation-delay: 0.3s; }

See our CSS animation guide for the animation-property fundamentals behind patterns like this one.

Making clip-path responsive with custom properties

A fixed-pixel or fixed-percentage diagonal looks right at one viewport width and wrong at every other. Driving the shape from a CSS custom property lets you adjust the slope at a breakpoint without duplicating the whole clip-path declaration.

.section-diagonal { --slope: 60px; clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--slope)), 0 100%); } @media (max-width: 640px) { .section-diagonal { --slope: 30px; } }

The same idea works for swapping an entire shape, not just adjusting one value in it, which is useful when a decorative polygon crop makes sense on desktop but should fall back to a plain circle on small screens:

.avatar { clip-path: circle(50%); } @media (min-width: 768px) { .avatar { clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%); } }

Converting SVG and Figma exports to clip-path

Design tools like Figma export path data as absolute coordinates matching the size of the artboard you drew on, for example a path that spans roughly 0 to 200 on a 200px-wide frame. Pasted directly into CSS's path() function, those coordinates only look right at that exact pixel size, and will not scale if the element you apply them to is a different size.

The reliable fix is to skip path() and use an SVG clipPath element with clipPathUnits set to objectBoundingBox, which expects every coordinate as a fraction between 0 and 1 of the element's own box rather than a fixed pixel value. Divide each coordinate from your export by the artboard's width or height to normalize it:

<!-- Exported path on a 200x200 artboard, coordinates 0 to 200 --> <path d="M40,10 L180,90 L120,190 L20,150 Z" /> <!-- Normalized to 0 to 1 by dividing every value by 200 --> <svg width="0" height="0" style="position:absolute"> <clipPath id="figmaShape" clipPathUnits="objectBoundingBox"> <path d="M0.2,0.05 L0.9,0.45 L0.6,0.95 L0.1,0.75 Z" /> </clipPath> </svg> .element { clip-path: url(#figmaShape); }

This is exactly the format our clip-path generator's SVG output tab produces for its built-in curve presets, so if your shape is close to a blob, wave, arch, drop, or leaf, starting from one of those and adjusting the control points is usually faster than normalizing a Figma export by hand.

Using clip-path with Tailwind CSS

Tailwind does not ship built-in utility classes like clip-circle or clip-polygon, so clip-path is applied through Tailwind's arbitrary value syntax: square brackets containing the raw CSS property and value, with any spaces in the value replaced by underscores.

<div class="[clip-path:polygon(50%_0%,100%_100%,0%_100%)]"></div>

State and breakpoint variants work the same way they do for any other Tailwind utility, which makes the hover reveal pattern from earlier a single line of markup:

<div class="[clip-path:circle(0%_at_50%_50%)] hover:[clip-path:circle(75%_at_50%_50%)] transition-[clip-path] duration-500"></div>

For a shape you reuse across several components, define it once as a CSS custom property in your global stylesheet and reference the variable in the arbitrary value instead of repeating the full polygon string in your markup every time:

:root { --diagonal: polygon(0 0, 100% 0, 100% 85%, 0 100%); }
<div class="[clip-path:var(--diagonal)]"></div>

Using clip-path in React

Inline styles in React use the camelCase property name clipPath, not clip-path, the same way any hyphenated CSS property is written in a style object.

function ClipCard({ shape }) { return ( <div style={{ clipPath: shape, transition: 'clip-path 0.3s ease' }}> {/* content */} </div> ); }

Driving the shape from state turns a static crop into an interactive one, without needing a CSS :hover rule at all:

const [shape, setShape] = useState('circle(50%)'); <div style={{ clipPath: shape, transition: 'clip-path 0.3s ease' }} onMouseEnter={() => setShape('circle(75%)')} onMouseLeave={() => setShape('circle(50%)')} />

Inline styles are not autoprefixed. If your project still needs to support Safari versions older than 14, add a second WebkitClipPath key alongside clipPath with the same value, or switch to a CSS-in-JS library such as styled-components or Emotion, which autoprefix clip-path for you inside a normal template literal.

Debugging and performance

  • The element still occupies its original space. clip-path only changes what is painted, not the element's box for layout purposes. Clipping a box down to a small circle leaves the rest of its original rectangular footprint as empty space; nothing else will flow into it. If you need surrounding elements to reflow around the visible shape, clip-path is the wrong tool, you need to resize the box itself.
  • clip-path creates a new stacking context. Like transform, filter, and opacity below 1, applying clip-path to an element means its children's z-index values are only compared against each other, not against elements outside it. A child that should visually sit above something outside the clipped element can end up covered by it instead.
  • Complex shapes cost more to animate. circle() and ellipse() are cheap to animate since the browser only recomputes a few numbers per frame. A polygon() with many points, or a long path() string, costs more every frame since every point interpolates. For a plain reveal effect, animating transform and opacity together is usually cheaper; save clip-path animation for cases that genuinely need a non-rectangular shape to morph.
  • overflow: hidden is not a substitute. clip-path shapes a single element's own box. overflow: hidden on a parent only clips children to that parent's rectangle. The two are often combined, for example the reveal wipe pattern above, but neither replaces the other.

Frequently Asked Questions

What is the difference between clip-path and mask-image?

clip-path cuts an element to a vector shape, such as a polygon, circle, or path. The result is always hard-edged: a pixel is either fully visible or fully hidden, with no in-between. mask-image uses an image or gradient to control visibility instead, so it supports soft, partial transparency, like fading an element out at one edge. Reach for clip-path when you need a clean geometric cutout, and mask-image when you need a gradient, feathered edge, or textured reveal.

Can clip-path create a wavy or curved divider without SVG?

Not with the basic shape functions. polygon(), circle(), ellipse(), and inset() are all made of straight lines or simple arcs, so a smooth wave needs the path() function with bezier curve commands, which is effectively SVG path data written inline in CSS. It is usually faster to start from a ready-made curve preset, like the Wave preset in the Path or Curve tab of our clip-path generator, than to hand-write the bezier control points.

Does Tailwind CSS have a clip-path utility?

Not a built-in one as of the current release. Tailwind has no clip-circle or clip-polygon style utility classes out of the box, so clip-path is applied through Tailwind's arbitrary value syntax instead, for example class="[clip-path:circle(50%)]", with spaces in the value replaced by underscores. Hover, focus, and breakpoint variants work the same way as any other arbitrary value.

How do I animate clip-path shapes in React?

Set the clipPath property, camelCased, inside a style object or CSS-in-JS template, and pair it with a CSS transition on the clip-path property. Store the current shape in state and update it on an event like onMouseEnter to animate between two shapes. Inline styles do not get vendor prefixes automatically, so if you need to support Safari versions older than 14, add a second WebkitClipPath key alongside clipPath, or use a CSS-in-JS library that autoprefixes for you.

Why does an element clipped with clip-path still take up its original space in the layout?

clip-path only changes what is painted, not the element's box for layout purposes. Surrounding elements still see the element at its full, unclipped width and height, so clipping a box down to a small circle will leave the rest of its rectangular footprint as empty space that other elements will not flow into. If you need the layout itself to shrink to match the visible shape, clip-path is the wrong tool; you need to resize the actual box instead.

Does clip-path affect stacking order or z-index?

Yes. Applying clip-path to an element creates a new stacking context, the same way transform, filter, and opacity below 1 do. This means z-index values on that element's children are only compared against each other, not against elements outside the clipped element, which can cause a child that visually should sit on top of an outside element to be covered by it instead.

Is animating clip-path good or bad for performance?

Simple shapes like circle() and ellipse() animate cheaply because the browser only has to recompute a small set of numbers each frame. Complex polygon() shapes with many points, or path() shapes with long bezier data, cost more per frame since every point has to be interpolated. For a straightforward reveal effect, animating transform and opacity together is usually cheaper than animating a complex clip-path; save clip-path animation for cases where you specifically need a non-rectangular shape to morph.

How do I turn an SVG path exported from Figma into a working clip-path?

Figma and other design tools export path coordinates in absolute pixel values matching the artboard, which will not scale correctly if pasted directly into a CSS path() function on an element of a different size. Instead, place the path data inside an SVG clipPath element with clipPathUnits set to objectBoundingBox, and divide every coordinate by the artboard's width or height so each value falls between 0 and 1. Reference the result with clip-path: url(#id), and it will scale automatically with the element.

Build these shapes visually

Skip the manual coordinate math. Drag points, adjust sliders, and copy the CSS or SVG output instantly.

Open Clip-Path Generator →