Animating Border

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.

Method 1: Animating 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; }

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.

Method 2: Better 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; /* 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.

border artifact in safari

Method 3: Cut it with 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 { /* 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.

Method 4: linear-gradient background

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.

Method 5: Fake it with 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; /* 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:

  1. 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.
  2. 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.
  3. 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.


Animating Border is a post from CSS-Tricks

Animating Layouts with the FLIP Technique

User interfaces are most effective when they are intuitive and easily understandable to the user. Animation plays a major role in this – as Nick Babich said, animation brings user interfaces to life. However, adding meaningful transitions and micro-interactions is often an afterthought, or something that is “nice to have” if time permits. All too often, we experience web apps that simply “jump” from view to view without giving the user time to process what just happened in the current context.

This leads to unintuitive user experiences, but we can do better, by avoiding “jump cuts” and “teleportation” in creating UIs. After all, what’s more natural than real life, where nothing teleports (except maybe car keys), and everything you interact with moves with natural motion?

In this article, we’ll explore a technique called “FLIP” that can be used to animate the positions and dimensions of any DOM element in a performant manner, regardless of how their layout is calculated or rendered (e.g., height, width, floats, absolute positioning, transform, flexbox, grid, etc.)

Why the FLIP technique?

Have you ever tried to animate height, width, top, left, or any other properties besides transform and opacity? You might have noticed that the animations look a bit janky, and there’s a reason for that. When any property that triggers layout changes (such as `height`), the browser has to recursively check if any other element’s layout has changed as a result, and that can be expensive. If that calculation takes longer than one animation frame (around 16.7 milliseconds), then the animation frame will be skipped, resulting in “jank” since that frame wasn’t rendered in time. In Paul Lewis’ article “Pixels are Expensive”, he goes further in depth at how pixels are rendered and the various performance expenses.

In short, our goal is to be short – we want to calculate the least amount of style changes necessary, as quickly as possible. The key to this is only animating transform and opacity, and FLIP explains how we can simulate layout changes using only transform.

What is FLIP?

FLIP is a mnemonic device and technique first coined by Paul Lewis, which stands for First, Last, Invert, Play. His article contains an excellent explanation of the technique, but I’ll outline it here:

  • First: before anything happens, record the current (i.e., first) position and dimensions of the element that will transition. You can use element.getBoundingClientRect() for this, as will be shown below.
  • Last: execute the code that causes the transition to instantaneously happen, and record the final (i.e., last) position and dimensions of the element.*
  • Invert: since the element is in the last position, we want to create the illusion that it’s in the first position, by using transform to modify its position and dimensions. This takes a little math, but it’s not too difficult.
  • Play: with the element inverted (and pretending to be in the first position), we can move it back to its last position by setting its transform to none.

Below is how these steps can be implemented with the Web Animations API:

const elm = document.querySelector('.some-element'); // First: get the current bounds
const first = elm.getBoundingClientRect(); // execute the script that causes layout change
doSomething(); // Last: get the final bounds
const last = elm.getBoundingClientRect(); // Invert: determine the delta between the // first and last bounds to invert the element
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaW = first.width / last.width;
const deltaH = first.height / last.height; // Play: animate the final element from its first bounds
// to its last bounds (which is no transform)
elm.animate([{ transformOrigin: 'top left', transform: ` translate(${deltaX}px, ${deltaY}px) scale(${deltaW}, ${deltaH}) `
}, { transformOrigin: 'top left', transform: 'none'
}], { duration: 300, easing: 'ease-in-out', fill: 'both'
});

Note: At the time of writing, the Web Animations API is not yet supported in all browsers. However, you can use a polyfill.

See the Pen How the FLIP technique works by David Khourshid (@davidkpiano) on CodePen.

There are two important things to note:

  1. If the element’s size changed, you can transform scale in order to “resize” it with no performance penalty; however, make sure to set transformOrigin to 'top left' since that’s where we based our delta calculations.
  2. We’re using the Web Animations API to animate the element here, but you’re free to use any other animation engine, such as GSAP, Anime, Velocity, Just-Animate, Mo.js and more.

Shared Element Transitions

One common use-case for transitioning an element between app views and states is that the final element might not be the same DOM element as the initial element. In Android, this is similar to a shared element transition, except that the element isn’t “recycled” from view to view in the DOM as it is on Android.
Nevertheless, we can still achieve the FLIP transition with a little magic illusion:

const firstElm = document.querySelector('.first-element'); // First: get the bounds and then hide the element (if necessary)
const first = firstElm.getBoundingClientRect();
firstElm.style.setProperty('visibility', 'hidden'); // execute the script that causes view change
doSomething(); // Last: get the bounds of the element that just appeared
const lastElm = document.querySelector('.last-element');
const last = lastElm.getBoundingClientRect(); // continue with the other steps, just as before.
// remember: you're animating the lastElm, not the firstElm.

Below is an example of how two completely disparate elements can appear to be the same element using shared element transitions. Click one of the pictures to see the effect.

See the Pen FLIP example with WAAPI by David Khourshid (@davidkpiano) on CodePen.

Parent-Child Transitions

With the previous implementations, the element bounds are based on the window. For most use cases, this is fine, but consider this scenario:

  • An element changes position and needs to transition.
  • That element contains a child element, which itself needs to transition to a different position inside the parent.

Since the previously calculated bounds are relative to the window, our calculations for the child element are going to be off. To solve this, we need to ensure that the bounds are calculated relative to the parent element instead:

const parentElm = document.querySelector('.parent');
const childElm = document.querySelector('.parent > .child'); // First: parent and child
const parentFirst = parentElm.getBoundingClientRect();
const childFirst = childElm.getBoundingClientRect(); doSomething(); // Last: parent and child
const parentLast = parentElm.getBoundingClientRect();
const childLast = childElm.getBoundingClientRect(); // Invert: parent
const parentDeltaX = parentFirst.left - parentLast.left;
const parentDeltaY = parentFirst.top - parentLast.top; // Invert: child relative to parent
const childDeltaX = (childFirst.left - parentFirst.left) - (childLast.left - parentLast.left);
const childDeltaY = (childFirst.top - parentFirst.top) - (childLast.top - parentLast.top); // Play: using the WAAPI
parentElm.animate([ { transform: `translate(${parentDeltaX}px, ${parentDeltaY}px)` }, { transform: 'none' }
], { duration: 300, easing: 'ease-in-out' }); childElm.animate([ { transform: `translate(${childDeltaX}px, ${childDeltaY}px)` }, { transform: 'none' }
], { duration: 300, easing: 'ease-in-out' });

A few things to note here, as well:

  1. The timing options for the parent and child (duration, easing, etc.) do not necessarily need to match with this technique. Feel free to be creative!
  2. Changing dimensions in parent and/or child (width, height) was purposefully omitted in this example, since it is an advanced and complex topic. Let’s save that for another tutorial.
  3. You can combine the shared element and parent-child techniques for greater flexibility.

Using Flipping.js for Full Flexibility

The above techniques might seem straightforward, but they can get quite tedious to code once you have to keep track of multiple elements transitioning. Android eases this burden by:

  • baking shared element transitions into the core SDK
  • allowing developers to identify which elements are shared by using a common android:transitionName XML attribute

I’ve created a small library called Flipping.js with the same idea in mind. By adding a data-flip-key="..." attribute to HTML elements, it’s possible to predictably and efficiently keep track of elements that might change position and dimensions from state to state.

For example, consider this initial view:

 <section class="gallery"> <div class="photo-1" data-flip-key="photo-1"> <img src="/photo-1"/> </div> <div class="photo-2" data-flip-key="photo-2"> <img src="/photo-2"/> </div> <div class="photo-3" data-flip-key="photo-3"> <img src="/photo-3"/> </div> </section>

And this separate detail view:

 <section class="details"> <div class="photo" data-flip-key="photo-1"> <img src="/photo-1"/> </div> <p class="description"> Lorem ipsum dolor sit amet... </section>

Notice in the above example that there are 2 elements with the same data-flip-key="photo-1". Flipping.js tracks the “active” element by choosing the first element that meet these criteria:

  • The element exists in the DOM (i.e., it hasn’t been removed or detached)
  • The element is not hidden (hint: elm.getBoundingClientRect() will have { width: 0, height: 0 } for hidden elements)
  • Any custom logic specified in the selectActive option.

Getting Started with Flipping.js

There’s a few different packages for Flipping, depending on your needs:

  • flipping.js: tiny and low-level; only emits events when element bounds change
  • flipping.web.js: uses WAAPI to animate transitions
  • flipping.gsap.js: uses GSAP to animate transitions
  • More adapters coming soon!

You can grab the minified code directly from unpkg:

  • https://unpkg.com/flipping@latest/dist/flipping.js
  • https://unpkg.com/flipping@latest/dist/flipping.web.js
  • https://unpkg.com/flipping@latest/dist/flipping.gsap.js

Or you can npm install flipping --save and import it into your projects:

// import not necessary when including the unpkg scripts in a <script src="..."> tag
import Flipping from 'flipping/adapters/web'; const flipping = new Flipping(); // First: let Flipping read all initial bounds
flipping.read(); // execute the change that causes any elements to change bounds
doSomething(); // Last, Invert, Play: the flip() method does it all
flipping.flip();

Handling FLIP transitions as a result of a function call is such a common pattern, that the .wrap(fn) method transparently wraps (or “decorates”) the given function by first calling .read(), then getting the return value of the function, then calling .flip(), then returning the return value. This leads to much less code:

const flipping = new Flipping(); const flippingDoSomething = flipping.wrap(doSomething); // anytime this is called, FLIP will animate changed elements
flippingDoSomething();

Here is an example of using flipping.wrap() to easily achieve the shifting letters effect. Click anywhere to see the effect.

See the Pen Flipping Birthstones #Codevember by David Khourshid (@davidkpiano) on CodePen.

Adding Flipping.js to Existing Projects

In another article, we created a simple React gallery app using finite state machines. It works just as expected, but the UI could use some smooth transitions between states to prevent “jumping” and improve the user experience. Let’s add Flipping.js into our React app to accomplish this. (Keep in mind, Flipping.js is framework-agnostic.)

Step 1: Initialize Flipping.js

The Flipping instance will live on the React component itself, so that it’s isolated to only changes that occur within that component. Initialize Flipping.js by setting it up in the componentDidMount lifecycle hook:

 componentDidMount() { const { node } = this; if (!node) return; this.flipping = new Flipping({ parentElement: node }); // initialize flipping with the initial bounds this.flipping.read(); }

By specifying parentElement: node, we’re telling Flipping to only look for elements with a data-flip-key in the rendered App, instead of the entire document.
Then, modify the HTML elements with the data-flip-key attribute (similar to React’s key prop) to identify unique and “shared” elements:

 renderGallery(state) { return ( <section className="ui-items" data-state={state}> {this.state.items.map((item, i) => <img src={item.media.m} className="ui-item" style={{'--i': i}} key={item.link} onClick={() => this.transition({ type: 'SELECT_PHOTO', item })} data-flip-key={item.link} /> )} </section> ); } renderPhoto(state) { if (state !== 'photo') return; return ( <section className="ui-photo-detail" onClick={() => this.transition({ type: 'EXIT_PHOTO' })}> <img src={this.state.photo.media.m} className="ui-photo" data-flip-key={this.state.photo.link} /> </section> ) }

Notice how the img.ui-item and img.ui-photo are represented by data-flip-key={item.link} and data-flip-key={this.state.photo.link} respectively: when the user clicks on an img.ui-item, that item is set to this.state.photo, so the .link values will be equal.

And since they are equal, Flipping will smoothly transition from the img.ui-item thumbnail to the larger img.ui-photo.

Now we need to do two more things:

  1. call this.flipping.read() whenever the component will update
  2. call this.flipping.flip() whenever the component did update

Some of you might have already guessed where these method calls are going to occur: componentWillUpdate and componentDidUpdate, respectively:

 componentWillUpdate() { this.flipping.read(); } componentDidUpdate() { this.flipping.flip(); }

And, just like that, if you’re using a Flipping adapter (such as flipping.web.js or flipping.gsap.js), Flipping will keep track of all elements with a [data-flip-key] and smoothly transition them to their new bounds whenever they change. Here is the final result:

See the Pen FLIPping Gallery App by David Khourshid (@davidkpiano) on CodePen.

If you would rather implement custom animations yourself, you can use flipping.js as a simple event emitter. Read the documentation for more advanced use-cases.

Flipping.js and its adapters handle the shared element and parent-child transitions by default, as well as:

  • interrupted transitions (in adapters)
  • enter/move/leave states
  • plugin support for plugins such as mirror, which allows newly entered elements to “mirror” another element’s movement
  • and more planned in the future!

Resources

Similar libraries include:

  • FlipJS by Paul Lewis himself, which handles simple single-element FLIP transitions
  • React-Flip-Move, a useful React library by Josh Comeau
  • BarbaJS, not necessarily a FLIP library, but one that allows you to add smooth transitions between different URLs, without page jumps.

Further resources:

  • Animating the Unanimatable – Joshua Comeau
  • FLIP your Animations – Paul Lewis
  • Pixels are Expensive – Paul Lewis
  • Improving User Flow Through Page Transitions – Luigi de Rosa
  • Smart Transitions in User Experience Design – Adrian Zumbrunnen
  • What Makes a Good Transition? – Nick Babich
  • Motion Guidelines in Google’s Material Design
  • Shared Element Transition with React Native

Animating Layouts with the FLIP Technique is a post from CSS-Tricks