Transitioning border
for a hover state. Simple, right? You might be unpleasantly surprised.
The Challenge
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
- Single element (no helper divs, but psuedo-elements are allowed)
- CSS only (no JavaScript)
- Works for any size (not restricted to a specific width, height, or aspect ratio)
- Supports transparent backgrounds
- Smooth and performant transition
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
Method 1: Animating 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; }
See the Pen by Shaw (@shshaw) on CodePen.
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
Method 2: Better 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; /* Increased margin since the outline expands outside the element */
}
.border-button:hover { outline-width: 10px; }
See the Pen by Shaw (@shshaw) on CodePen.
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
.
See the Pen by Shaw (@shshaw) on CodePen.
Strangely, Safari often doesn’t render the outline
transition and occasionally leaves crazy artifacts.
clip-path
Method 3: Cut it with 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 {
/* Full width border and a clip-path visually cutting it down to the starting size */
border: solid 10px #FC5185;
clip-path: polygon(
calc(0% + 5px) calc(0% + 5px), /* top left */
calc(100% - 5px) calc(0% + 5px), /* top right */
calc(100% - 5px) calc(100% - 5px), /* bottom right */
calc(0% + 5px) calc(100% - 5px) /* bottom left */
);
transition: clip-path 0.6s linear;
}
.border-button:hover {
/* Clip-path spanning the entire box so it's no longer hiding the full-width border. */
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
See the Pen by Shaw (@shshaw) on CodePen.
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
background
Method 4: We 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 values will repeat so we only need to declare them once */
background-size:
calc(100% - 10px) 5px, /* top & bottom */
5px calc(100% - 10px); /* right & left */
background-position:
5px 5px, /* top */
calc(100% - 5px) 5px, /* right */
5px calc(100% - 5px), /* bottom */
5px 5px; /* left */
/* Since we're sizing and positioning with the above properties, we only need to set up a simple solid-color gradients for each side */
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%;
}
See the Pen by Shaw (@shshaw) on CodePen.
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
Method 5: Fake it with 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; /* Increased margin since the box-shado expands outside the element, like outline */
}
.border-button:hover { box-shadow: 0px 0px 0px 10px #FC5185; }
See the Pen by Shaw (@shshaw) on CodePen.
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
.
Pseudo-Elements
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); }
See the Pen by Shaw (@shshaw) on CodePen.
There are a few issues:
- The border will show through a transparent button. I forced a background on the button to show how the border is hiding behind the button. If your design calls for buttons with a full background, then this could work.
- You can’t scale the border to specific sizes. Since the button’s dimensions vary with the text, there’s no way to animate the border from exactly 5px to 10px using only CSS. In this example I’ve done some magic-numbers on the
scale
to get it to appear right, but that won’t be universal. - The border animates unevenly because the button’s aspect ratio isn’t 1:1. This usually means the left/right will appear larger than the top/bottom until the animation completes. This may not be an issue depending on how fast your transition is, the button’s aspect ratio, and how big your border is.
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.
Beyond CSS
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:
- Creative Buttons
- Button Styles Inspiration
- Animated Checkboxes
- Distorted Button Effects
- Progress Button Styles
Conclusion
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.
See the Pen by Shaw (@shshaw) on CodePen.
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.
On the box shadow, I can see the box after de-hovering.
Great method comparison! Thank you.
What browser / platform, Umar? In my tests, I didn’t run into any artifact trouble like outline had in Safari.
Same here. Seems like a Chrome issue, tried it in Firefox and didn’t notice that issue.
In Firefox Quantum the is the same problem here :/
I can see this occasionally with
box-shadow
in Chrome, but haven’t run into it with Firefox or Safari.One solution I found is using
outline: solid 10px transparent
, where 10px is the total expanded border size. This seems to clear up Chrome’s rendering artifacts:Only problem I see with this is that the animation on some mobile devices is a little jittery. Though, this was in an older version of Chrome.
Really enjoyed this post! Thanks for the awesome content as always. :)
Thanks, Joe!
The techniques definitely have some jitter or feel stepped across different browsers. The “Animation” column in my overview table is mostly rating this.
So satisfying article. I love how each concept is briefly explained.
This site always comes with finely crafted content and that’s why it’s the only site of which I’ve subscribed for notifications
Fawad Tariq
UI/UX Designer & Front-End Developer
I agree with you on this, explained very well!
I’ve might overlooked this solution, but why not just animate the border and padding at the same time.
Good thought, Jermaine!
Since
border
values are rounded, you’re still going to have the stepped animation that I covered. Also, when taking away padding to expand the border, the border comes in toward the center of the element instead of going out from the element.Both of those aren’t a big deal, but not exactly the effect I was going for.
The biggest concern with this way is performance.
border
changes trigger layout, and combining that with changingpadding
means its more expensive to render & paint, though the actual page layout doesn’t visually change.Your solution is the only one that enlarges the border towards the inside of the button, which is interesting.
As for the padding thing, the increase of the border is of 1rem (0.5rem to 1rem on two sides), to allow for this space on the left and right sides, you just need to make room for 1rem, spread on two sides, so just 0.5rem less for your padding.
HTH
Am I missing something here? Why not just use (1) with box-sizing: border-box; ?
Since the buttons don’t have set dimensions (
width: 200px
, etc. ), the change in border size will still change the layout.Without looking at your solutions, just with the initial requirements I first went with the strict animation of the border (and of a margin to compensate for the space taken around), and then to the box-shadow.
This codepen (https://codepen.io/mcbenny/pen/pdBVxd) shows the two effects.
One interesting thing with the
box-shadow
approach is that theborder-radius
also applies “inside” the box, which is prettier in my opinion (if rounded corners are still a thing ;-)But the
border
technique allows for “double” style borders…Nice demo, Ben!
box-shadow
is my preferred option as well.Great note about the
box-shadow
keepingborder-radius
. As you pointed out,border
does have a lot more fine-grained control (individual sides, alternative styles, etc) but the performance and animation quality are severely lacking right now. Hopefully browsers will continue optimizingborder
to give us better control in the future.What about pseudo element (button can’t be transparent though)
See the Pen JOQQvK by Janusz Kaliszczak (@kali187) on CodePen.
https://production-assets.codepen.io/assets/embed/ei.js
Nice idea, Janusz! I left out similar methods due to the need for a background. The performance here is also going to be a rather poor since you’re animating the position, and unfortunately
top
,left
, etc. are all rounded to whole values as well, so you get that stepped effect I talk about in the first two methods.Maybe like this then?
See the Pen gXVroP by Janusz Kaliszczak (@kali187) on CodePen.
Firefox 57 renders Method 2 quite smoothly. That doesn’t solve the problem, but I find it interesting.