I recently shared this component on X, and a lot of people liked it. This article briefly explains how it's built.
Press and hold the button to see the transition.
Our starting point is a simple, styled button. You can use the code editor below to follow along.
"use client"; export default function ClipPathButton() { return ( <button className="button"> <svg height="16" strokeLinejoin="round" viewBox="0 0 16 16" width="16"> <path fillRule="evenodd" clipRule="evenodd" d="M6.75 2.75C6.75 2.05964 7.30964 1.5 8 1.5C8.69036 1.5 9.25 2.05964 9.25 2.75V3H6.75V2.75ZM5.25 3V2.75C5.25 1.23122 6.48122 0 8 0C9.51878 0 10.75 1.23122 10.75 2.75V3H12.9201H14.25H15V4.5H14.25H13.8846L13.1776 13.6917C13.0774 14.9942 11.9913 16 10.6849 16H5.31508C4.00874 16 2.92263 14.9942 2.82244 13.6917L2.11538 4.5H1.75H1V3H1.75H3.07988H5.25ZM4.31802 13.5767L3.61982 4.5H12.3802L11.682 13.5767C11.6419 14.0977 11.2075 14.5 10.6849 14.5H5.31508C4.79254 14.5 4.3581 14.0977 4.31802 13.5767Z" fill="currentColor" /> </svg> Hold to Delete </button> ); }
Before we take care of the reveal transition, we need two versions of the button: the default monochrome one, and the version we want to gradually reveal.
<button className="button">
<div aria-hidden="true" className="hold-overlay">
<TrashIcon />
Hold to Delete
</div>
<TrashIcon />
Hold to Delete
</button>
<button className="button">
<div aria-hidden="true" className="hold-overlay">
<TrashIcon />
Hold to Delete
</div>
<TrashIcon />
Hold to Delete
</button>
In this case, .button
has position: relative
and .hold-overlay
is absolutely positioned on top of it. This creates the following effect:
Before revealing the overlay, we need to hide it first. We’ll use the inset
shape of the clip-path
property and set the right value to 100%. You can think of each inset
value as a side of a rectangle, similar to the margin or padding shorthand.
.hold-overlay {
clip-path: inset(0px 100% 0px 0px);
}
.hold-overlay {
clip-path: inset(0px 100% 0px 0px);
}
This hides the red overlay from the right. Why from the right? Because by subtracting from our 100%, we can reveal the button from the left, which will create the effect we want. I cover clip-path
in more detail in The Magic of Clip Path.
To reveal it, we'll use the :active
pseudo-class. While the user holds the button, we'll set clip-path
to inset(0px 0px 0px 0px)
.
.button:active .hold-overlay {
clip-path: inset(0px 0px 0px 0px);
}
.button:active .hold-overlay {
clip-path: inset(0px 0px 0px 0px);
}
This will make the overlay visible instantly once the button is pressed:
To add an animation, we'll use the transition
property. Since we want a longer effect, we’ll use a duration of 2s
. We should use linear
timing function to reveal the overlay evenly.
.hold-overlay {
transition: clip-path 2s linear;
}
.hold-overlay {
transition: clip-path 2s linear;
}
That's it! Let's now add some polish to our button to make it feel better.
The first thing that feels off is the transition when you release the button. Pressing should be slow to allow the user to confirm their choice, but the release can be much snappier.
To do that, I'll set the .hold-overlay
transition to 200ms
with an ease-out
timing function. Then, in the :active
state I'll override it with a 2s
transition using a linear
timing function.
.hold-overlay {
transition: clip-path 200ms ease-out;
}
.button:active .hold-overlay {
transition: clip-path 2s linear;
}
.hold-overlay {
transition: clip-path 200ms ease-out;
}
.button:active .hold-overlay {
transition: clip-path 2s linear;
}
This makes the release transition much faster while keeping the press transition slow enough for the user to confirm their choice. Remember: you should make your animations fast (most of the time).
One thing that makes any button feel better is a slight scale-down animation on press. It feels good because it gives the user instant feedback, making the UI feel more responsive, as if it's really listening to the user.
To do that, I'll add transform: scale(0.97)
to the button's :active
state and apply a snappy transition.
.button {
transition: transform 160ms ease-out;
}
.button:active {
transform: scale(0.97);
}
.button {
transition: transform 160ms ease-out;
}
.button:active {
transform: scale(0.97);
}
This gives us a nice hold to delete button. Here's the final code:
"use client"; export default function ClipPathButton() { return ( <button className="button"> <div aria-hidden="true" className="hold-overlay"> <svg height="16" strokeLinejoin="round" viewBox="0 0 16 16" width="16"> <path fillRule="evenodd" clipRule="evenodd" d="M6.75 2.75C6.75 2.05964 7.30964 1.5 8 1.5C8.69036 1.5 9.25 2.05964 9.25 2.75V3H6.75V2.75ZM5.25 3V2.75C5.25 1.23122 6.48122 0 8 0C9.51878 0 10.75 1.23122 10.75 2.75V3H12.9201H14.25H15V4.5H14.25H13.8846L13.1776 13.6917C13.0774 14.9942 11.9913 16 10.6849 16H5.31508C4.00874 16 2.92263 14.9942 2.82244 13.6917L2.11538 4.5H1.75H1V3H1.75H3.07988H5.25ZM4.31802 13.5767L3.61982 4.5H12.3802L11.682 13.5767C11.6419 14.0977 11.2075 14.5 10.6849 14.5H5.31508C4.79254 14.5 4.3581 14.0977 4.31802 13.5767Z" fill="currentColor" /> </svg> Hold to Delete </div> <svg height="16" strokeLinejoin="round" viewBox="0 0 16 16" width="16"> <path fillRule="evenodd" clipRule="evenodd" d="M6.75 2.75C6.75 2.05964 7.30964 1.5 8 1.5C8.69036 1.5 9.25 2.05964 9.25 2.75V3H6.75V2.75ZM5.25 3V2.75C5.25 1.23122 6.48122 0 8 0C9.51878 0 10.75 1.23122 10.75 2.75V3H12.9201H14.25H15V4.5H14.25H13.8846L13.1776 13.6917C13.0774 14.9942 11.9913 16 10.6849 16H5.31508C4.00874 16 2.92263 14.9942 2.82244 13.6917L2.11538 4.5H1.75H1V3H1.75H3.07988H5.25ZM4.31802 13.5767L3.61982 4.5H12.3802L11.682 13.5767C11.6419 14.0977 11.2075 14.5 10.6849 14.5H5.31508C4.79254 14.5 4.3581 14.0977 4.31802 13.5767Z" fill="currentColor" /> </svg> Hold to Delete </button> ); }
This component is coming from my animations course in which we build a lot of other things ranging from simple CSS animations to complex Framer Motion (now Motion) components.
Check out "Animations on the Web"Some of the components we build in the course.