Let’s Make a Rubber Button With HTML, CSS and SVG
At 4/19/2024
While I wasn’t looking, a rubber button I shared on CodePen was viewed more than 11,000 times (as of this writing). I thought it might be fun to break down how the effect works, including some accessibility and compatibility improvements over what I originally shared.
The End Result
Try hovering or pressing the button in a supported browser to see the rubber/elastic effect:
Step 1: SVG
To start, we need three SVG paths: One for the container’s default state, one for the hover state, and one for when the button is pressed down. It’s important that all three states have the same number of points to transition between: For this reason, I personally have an easier time designing these paths by hand instead of in a design tool.
I designed my shapes based on a 100-pixel square to make math a bit easier:
Here’s an example SVG with all three paths accounted for. We’ll need the first path for our markup, and the other two for our styles.
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
<!-- default -->
<path d="M0,0 C0,0 100,0 100,0 C100,0 100,100 100,100 C100,100 0,100 0,100 C0,100 0,0 0,0 z" />
<!-- hover -->
<path d="M0,0 C0,-5 100,-5 100,0 C105,0 105,100 100,100 C100,105 0,105 0,100 C-5,100 -5,0 0,0 z" />
<!-- active -->
<path d="M0,0 C30,10 70,10 100,0 C95,30 95,70 100,100 C70,90 30,90 0,100 C5,70 5,30 0,0 z" />
</svg>
Code language: HTML, XML (xml)
Step 2: Markup
Our rubber button will consist of these elements:
- One real, actual
button
element. Hooray for native functionality! - An
svg
element containing the default path of our containing shape. We’ll want to usearia-hidden="true"
since this is decorative, andpreserveAspectRatio="none"
so it will freely stretch to match the size of our button. - A container for the normal button text content so we can animate it independently.
Here’s what that markup looks like:
<button class="button">
<svg class="button__shape"
viewBox="0 0 100 100"
preserveAspectRatio="none"
aria-hidden="true">
<path class="button__path" d="M0,0 C0,0 100,0 100,0 C100,0 100,100 100,100 C100,100 0,100 0,100 C0,100 0,0 0,0 z"/>
</svg>
<span class="button__content">
Hello world!
</span>
</button>
Code language: HTML, XML (xml)
Step 3: Default Styles
Let’s begin styling the button itself. This may look familiar to those who’ve customized native HTML buttons before, with two exceptions:
- We’ll use
relative
positioning so we can position the SVG shape relative to this container. - We’ll set
background
totransparent
, allowing the aforementioned shape to show through.
.button {
appearance: none;
background: transparent;
border: 0;
color: #fff;
cursor: pointer;
font: inherit;
font-weight: 500;
line-height: 1;
padding: 1em 1.5em;
position: relative;
}
Code language: CSS (css)
Next, we’ll set the button content’s display
(so it will respond to transformations correctly) and position
(so it will remain visible above the SVG):
.button__content {
display: block;
position: relative;
}
Code language: CSS (css)
And finally, we’ll position our SVG so that it fills the button. We’ll also set its fill color and allow its contents to visibly overflow while animating:
.button__shape {
block-size: 100%;
fill: #950cde;
inline-size: 100%;
inset: 0;
overflow: visible;
position: absolute;
}
Code language: CSS (css)
And with that, we’ve achieved the unassuming default state of our rubber button. Time for some animated transitions!
Step 4: Transitions
Let’s start with some custom properties to keep certain aspects of our animated transitions in sync. We’ll use an ease out back bezier function for a bit of springy-ness, and we’ll plan to scale elements by 5% (since our SVG paths tend to stretch outward by 5 out of 100 pixels):
:root {
--button-motion-ease: cubic-bezier(0.34, 1.56, 0.64, 1);
--button-motion-duration: 0.3s;
--button-scale-up: 1.05;
--button-scale-down: 0.95;
}
Code language: CSS (css)
We should set the duration to 0s
for those with motion sensitivity:
@media (prefers-reduced-motion: reduce) {
:root {
--button-motion-duration: 0s;
}
}
Code language: CSS (css)
With these properties in place, we can start adding some transitions.
To the button itself, we can use a brightness filter to shade or highlight the overall shape depending on how close it appears to the viewer:
.button {
transition: filter var(--button-motion-duration) var(--button-motion-ease);
}
.button:hover {
filter: brightness(1.1);
}
.button:active {
filter: brightness(0.9);
}
Code language: CSS (css)
Next, let’s animate the button’s content, scaling up or down to visibly expand or recede:
.button__content {
transition: transform var(--button-motion-duration) var(--button-motion-ease);
}
.button:hover .button__content {
transform: scale(var(--button-scale-up));
}
.button:active .button__content {
transform: scale(var(--button-scale-down));
}
Code language: CSS (css)
As of this writing, Safari does not support the CSS path()
function. As a fallback, we can apply the same scale transition to our SVG as the button content:
.button__shape {
transition: transform var(--button-motion-duration) var(--button-motion-ease);
}
@supports not (d: path('')) {
.button:hover .button__shape {
transform: scale(var(--button-scale-up));
}
.button:active .button__shape {
transform: scale(var(--button-scale-down));
}
}
Code language: CSS (css)
And finally, our pièce de résistance… animating the path data! We can insert the value of our alternate path shape’s d
attributes into a path()
function for our :hover
and :active
states:
.button__path {
transition: d var(--button-motion-duration) var(--button-motion-ease);
}
.button:hover .button__path {
d: path("M0,0 C0,-5 100,-5 100,0 C105,0 105,100 100,100 C100,105 0,105 0,100 C-5,100 -5,0 0,0 z");
}
.button:active .button__path {
d: path("M0,0 C30,10 70,10 100,0 C95,30 95,70 100,100 C70,90 30,90 0,100 C5,70 5,30 0,0 z");
}
Code language: CSS (css)
And voilà, our rubber button is complete!
Taking It Further
There are a lot of different ways we could remix this starting point:
- There’s nothing about this technique that constrains us to flat colors: Experiment with gradients, strokes and other effects!
- You could use scroll-driven animations to make the button morph as it scrolls in or out of view.
- If we want to morph between paths with incompatible points, there are quite a few JavaScript solutions available: I’d recommend checking out GreenSock’s MorphSVG plugin, Polymorph and KUTE.js.
- If we want to use this same pattern in multiple places, it might make a good progressively enhanced web component.
Effects like this can easily overwhelm an interface if overused. But applied purposefully, they can inject a bit of fun into an otherwise banal or forgettable interaction.