Build Design Systems With Penpot Components
Penpot's new component system for building scalable design systems, emphasizing designer-developer collaboration.

medium bookmark / Raindrop.io |
Transitioning border
for a hover state. Simple, right? You might be unpleasantly surprised.
The challenge is simple: building a button with an expanding border on hover.
This article will focus on genuine CSS tricks that would be easy to drop into any project without having to touch the DOM or use JavaScript. The methods covered here will follow these rules
I proposed this challenge in the Animation at Work Slack and again on Twitter. Though there was no consensus on the best approach, I did receive some really clever ideas by some phenomenal developers.
border
The most straightforward way to animate a border is… well, by animating border
.
.border-button {
border: solid 5px #FC5185;
transition: border-width 0.6s linear;
}
.border-button:hover { border-width: 10px; }
Nice and simple, but there are some big performance issues.
Since border
takes up space in the document’s layout, changing the border-width
will trigger layout. Nearby elements will shift around because of the new border size, making browser reposition those elements every frame of the animation unless you set an explicit size on the button.
As if triggering layout wasn’t bad enough, the transition itself feels “stepped”. I’ll show why in the next example.
border
with outline
How can we change the border without triggering layout? By using outline
instead! You’re probably most familiar with outline
from removing it on :focus
styles (though you shouldn’t), but outline
is an outer line that doesn’t change an element’s size or position in the layout.
.border-button {
outline: solid 5px #FC5185;
transition: outline 0.6s linear;
margin: 0.5em;
}
.border-button:hover { outline-width: 10px; }
A quick check in Dev Tools’ Performance tab shows the outline
transition does not trigger layout. Regardless, the movement still seems stepped because browsers are rounding the border-width
and outline-width
values so you don’t get sub-pixel rendering between 5
and 6
or smooth transitions from 5.4
to 5.5
.
Strangely, Safari often doesn’t render the outline
transition and occasionally leaves crazy artifacts.
clip-path
First implemented by Steve Gardner, this method uses clip-path
with calc
to trim the border down so on hover we can transition to reveal the full border.
.border-button {
border: solid 10px #FC5185;
clip-path: polygon(
calc(0% + 5px) calc(0% + 5px),
calc(100% - 5px) calc(0% + 5px),
calc(100% - 5px) calc(100% - 5px),
calc(0% + 5px) calc(100% - 5px)
);
transition: clip-path 0.6s linear;
}
.border-button:hover {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
clip-path
technique is the smoothest and most performant method so far, but does come with a few caveats. Rounding errors may cause a little unevenness, depending on the exact size. The border also has to be full size from the start, which may make exact positioning tricky.
Unfortunately there’s no IE/Edge support yet, though it seems to be in development. You can and should encourage Microsoft’s team to implement those features by voting for masks/clip-path to be added.
linear-gradient
backgroundWe can simulate a border using a clever combination of multiple linear-gradient
backgrounds properly sized. In total we have four separate gradients, one for each side. The background-position
and background-size
properties get each gradient in the right spot and the right size, which can then be transitioned to make the border expand.
.border-button {
background-repeat: no-repeat;
background-size:
calc(100% - 10px) 5px,
5px calc(100% - 10px);
background-position:
5px 5px,
calc(100% - 5px) 5px,
5px calc(100% - 5px),
5px 5px;
background-image:
linear-gradient(0deg, #FC5185, #FC5185),
linear-gradient(0deg, #FC5185, #FC5185),
linear-gradient(0deg, #FC5185, #FC5185),
linear-gradient(0deg, #FC5185, #FC5185);
transition: all 0.6s linear;
transition-property: background-size, background-position;
}
.border-button:hover {
background-position: 0 0, 100% 0, 0 100%, 0 0;
background-size: 100% 10px, 10px 100%, 100% 10px, 10px 100%;
}
This method is quite difficult to set up and has quite a few cross-browser differences. Firefox and Safari animate the faux-border smoothly, exactly the effect we’re looking for. Chrome’s animation is jerky and even more stepped than the outline
and border
transitions. IE and Edge refuse to animate the background
at all, but they do give the proper border expansion effect.
box-shadow
Hidden within box-shadow
‘s spec is a fourth value for spread-radius
. Set all the other length values to 0px
and use the spread-radius to build your border
alternative that, like outline
, won’t affect layout.
.border-button {
box-shadow: 0px 0px 0px 5px #FC5185;
transition: box-shadow 0.6s linear;
margin: 0.5em;
}
.border-button:hover { box-shadow: 0px 0px 0px 10px #FC5185; }
The transition with box-shadow
is adequately performant and feels much smoother, except in Safari where it’s snapping to whole-values during the transition like border
and outline
.
Several of these techniques can be modified to use a pseudo-element instead, but pseudo-elements ended up causing some additional performance issues in my tests.
For the box-shadow
method, the transition occasionally triggered paint in a much larger area than necessary. Reinier Kaper pointed out that a pseudo-element can help isolate the paint to a more specific area. As I ran further tests, box-shadow
was no longer causing paint in large areas of the document and the complication of the pseudo-element ended up being less performant. The change in paint and performance may have been due to a Chrome update, so feel free to test for yourself.
I also could not find a way to utilize pseudo-elements in a way that would allow for transform
based animation.
Why not transform: scale
?
You may be firing up Twitter to helpfully suggest using transform: scale
for this. Since transform
and opacity
are the best style properties to animate for performance, why not use a pseudo-element and have the border scale up & down?
.border-button {
position: relative;
margin: 0.5em;
border: solid 5px transparent;
background: #3E4377;
}
.border-button:after {
content: '';
display: block;
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
border: solid 10px #FC5185;
margin: -15px;
z-index: -1;
transition: transform 0.6s linear;
transform: scale(0.97, 0.93);
}
.border-button:hover::after { transform: scale(1,1); }
There are a few issues:
scale
to get it to appear right, but that won’t be universal.If your button has set dimensions, Cher pointed out a clever way to calculate the exact scales needed, though it may be subject to some rounding errors.
If we loosen our rules a bit, there are many interesting ways you can animate borders. Codrops consistently does outstanding work in this area, usually utilizing SVGs and JavaScript. The end results are very satisfying, though they can be a bit complex to implement. Here are a few worth checking out:
There’s more to borders than simply border
, but if you want to animate a border you may have some trouble. The methods covered here will help, though none of them are a perfect solution. Which you choose will depend on your project’s requirements, so I’ve laid out a comparison table to help you decide.
My recommendation would be to use box-shadow,
which has the best overall balance of ease-of-implementation, animation effect, performance and browser support.
Do you have another way of creating an animated border? Perhaps a clever way to utilize transforms for moving a border? Comment below or reach me on Twitter to share your solution to the challenge.
Special thanks to Martin Pitt, Steve Gardner, Cher, Reinier Kaper, Joseph Rex, David Khourshid, and the Animation at Work community.
AI-driven updates, curated by humans and hand-edited for the Prototypr community