Path Clipping with shape()
Define responsive clipping paths with Bézier curves (curve to) and arcs (arc to) directly in CSS using the shape() function.
Key Implementation Points
Markup Structure
Control active clipping shape on a single card element using radio inputs and :has() pseudo-class.
<main class="scene">
<fieldset class="controls">
<legend class="sr-only">Choose Shape</legend>
<label class="control-btn">
<input type="radio" name="shape" value="wave" checked>
<span>Wave</span>
</label>
<label class="control-btn">
<input type="radio" name="shape" value="arch">
<span>Arch</span>
</label>
<label class="control-btn">
<input type="radio" name="shape" value="ticket">
<span>Ticket</span>
</label>
<label class="control-btn">
<input type="radio" name="shape" value="blob">
<span>Blob</span>
</label>
</fieldset>
<div class="shape-card">
<img src="/images/rod-long-gUYYvPrnuHY-unsplash.jpg" alt="Whale in deep water" class="shape-image">
<div class="shape-caption">
<span class="shape-label">CSS shape()</span>
<h2 class="shape-heading">Native Path Clipping</h2>
</div>
</div>
</main>
Wave Clipping with Bézier Curves (curve to)
Set pen position with from <x> <y>, draw straight edges with line to, and generate smooth S-curves with curve to <end> with <control1> / <control2>. Responsive percentages scale automatically with container dimensions, and control points update smoothly on hover.
/* 1. Wave: Bézier curve along the bottom edge */
.scene:has(input[value="wave"]:checked) .shape-card {
clip-path: shape(
from 0 0,
line to 100% 0,
line to 100% 82%,
curve to 0% 82% with 65% 100% / 35% 65%,
close
);
}
/* Hover ripple animation on wave */
.scene:has(input[value="wave"]:checked) .shape-card:hover {
clip-path: shape(
from 0 0,
line to 100% 0,
line to 100% 82%,
curve to 0% 82% with 65% 65% / 35% 100%,
close
);
}
Arch Shape with Arcs (arc to)
Create a semi-circular top arch using arc to <end> of <radius> [cw | ccw].
/* 2. Arch: Semi-circular top dome */
.scene:has(input[value="arch"]:checked) .shape-card {
clip-path: shape(
from 0 35%,
arc to 100% 35% of 50% cw,
line to 100% 100%,
line to 0 100%,
close
);
}
Ticket Notch Cutout (arc to + calc)
Carve inward semi-circular notches into the element boundary by combining arc to with calc() for precision positioning.
/* 3. Ticket: Symmetric semi-circular side notches */
.scene:has(input[value="ticket"]:checked) .shape-card {
clip-path: shape(
from 0 0,
line to 100% 0,
line to 100% calc(50% - 20px),
arc to 100% calc(50% + 20px) of 20px ccw,
line to 100% 100%,
line to 0 100%,
line to 0 calc(50% + 20px),
arc to 0 calc(50% - 20px) of 20px ccw,
close
);
}
Shape Morphing with Transitions
Smoothly interpolate between different shape() paths with standard transition: clip-path.
/* 4. Blob: Organic Bézier curves */
.scene:has(input[value="blob"]:checked) .shape-card {
clip-path: shape(
from 30% 0%,
curve to 100% 30% with 70% 0% / 100% 10%,
curve to 70% 100% with 100% 70% / 90% 100%,
curve to 0% 70% with 40% 100% / 0% 90%,
curve to 30% 0% with 0% 30% / 10% 0%,
close
);
} <main class="scene">
<fieldset class="controls">
<legend class="sr-only">Choose Shape</legend>
<label class="control-btn">
<input type="radio" name="shape" value="wave" checked>
<span>Wave</span>
</label>
<label class="control-btn">
<input type="radio" name="shape" value="arch">
<span>Arch</span>
</label>
<label class="control-btn">
<input type="radio" name="shape" value="ticket">
<span>Ticket</span>
</label>
<label class="control-btn">
<input type="radio" name="shape" value="blob">
<span>Blob</span>
</label>
</fieldset>
<div class="shape-card">
<img src="/images/rod-long-gUYYvPrnuHY-unsplash.jpg" alt="Whale in deep water" class="shape-image">
<div class="shape-caption">
<span class="shape-label">CSS shape()</span>
<h2 class="shape-heading">Native Path Clipping</h2>
</div>
</div>
</main>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #f8fafc;
background-image:
linear-gradient(#e2e8f0 1px, transparent 1px),
linear-gradient(90deg, #e2e8f0 1px, transparent 1px);
background-size: 24px 24px;
color: #0f172a;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.scene {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.5rem;
min-height: 100vh;
padding: 1.5rem;
}
/* Controls */
.controls {
display: flex;
gap: 0.375rem;
border: none;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(8px);
padding: 0.375rem;
border-radius: 9999px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.control-btn {
display: inline-flex;
align-items: center;
padding: 0.375rem 0.875rem;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 600;
color: #64748b;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.control-btn input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.control-btn:has(input:checked) {
background: #0f172a;
color: #ffffff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Card */
.shape-card {
position: relative;
width: min(440px, 90vw);
aspect-ratio: 4 / 3;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
transition: clip-path 0.6s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
cursor: pointer;
}
.shape-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.shape-caption {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 1.5rem;
background: linear-gradient(to top, rgba(15, 23, 42, 0.85) 0%, rgba(15, 23, 42, 0.2) 50%, transparent 100%);
color: #ffffff;
pointer-events: none;
}
.shape-label {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: #ffffff;
margin-bottom: 0.25rem;
}
.shape-heading {
font-size: 1.25rem;
font-weight: 700;
line-height: 1.2;
}
/* 1. Wave: Bézier curve along the bottom edge */
.scene:has(input[value="wave"]:checked) .shape-card {
clip-path: shape(
from 0 0,
line to 100% 0,
line to 100% 82%,
curve to 0% 82% with 65% 100% / 35% 65%,
close
);
}
/* Hover ripple animation on wave */
.scene:has(input[value="wave"]:checked) .shape-card:hover {
clip-path: shape(
from 0 0,
line to 100% 0,
line to 100% 82%,
curve to 0% 82% with 65% 65% / 35% 100%,
close
);
}
/* 2. Arch: Semi-circular top dome */
.scene:has(input[value="arch"]:checked) .shape-card {
clip-path: shape(
from 0 35%,
arc to 100% 35% of 50% cw,
line to 100% 100%,
line to 0 100%,
close
);
}
/* 3. Ticket: Symmetric semi-circular side notches */
.scene:has(input[value="ticket"]:checked) .shape-card {
clip-path: shape(
from 0 0,
line to 100% 0,
line to 100% calc(50% - 20px),
arc to 100% calc(50% + 20px) of 20px ccw,
line to 100% 100%,
line to 0 100%,
line to 0 calc(50% + 20px),
arc to 0 calc(50% - 20px) of 20px ccw,
close
);
}
/* 4. Blob: Organic Bézier curves */
.scene:has(input[value="blob"]:checked) .shape-card {
clip-path: shape(
from 30% 0%,
curve to 100% 30% with 70% 0% / 100% 10%,
curve to 70% 100% with 100% 70% / 90% 100%,
curve to 0% 70% with 40% 100% / 0% 90%,
curve to 30% 0% with 0% 30% / 10% 0%,
close
);
}
/* Fallback for unsupported browsers */
@supports not (clip-path: shape(from 0 0, close)) {
.shape-card {
border-radius: 1rem;
}
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
</style>