Animated border


5 min read

Something which I've done recently is an animated border. It starts off as no border at all, then you see it drawing the border from one corner. Of course there's no simple way to animate the whole border.

It sounds complicated, the code is a bit complicated, but it's not once you understand how it's doing it.

The setup

All you need is an element that you're drawing the border around, and a way to start the border drawing. I've drawn the border around an image and a paragraph in the past, but for this demo I'm doing it around a div. I've added a background colour to the div and some padding on it so the border isn't touching it. I've added the border when you scroll to it in the past, but for this demo it adds the border when you click the button.

<button>Add border</button>
:root {
  --border-colour: black;  
  --border-thickness: 5px;
  --padding: 8px;
  --border-transition-time: 0ms;

@media screen and (prefers-reduced-motion: no-preference) { 
  :root {
    --border-transition-time: 250ms;

div {
  position: relative;
  width: 200px;
  height: 100px;
  top: 50px;
  left: 50px;
  background-color: pink;
  padding: var(--padding);

Here I've added some custom properties because we're going to be using those values multiple times. And I've just moved the div away from the edge a bit so it's easier to see. The only line of the div code you need on your element is the position: relative, everything is is just for this demo.

I've also added a reduce motion media query. This way, if anyone has reduced motion turned on they won't see the border animating. Instead the whole thing will just appear.

const button = document.querySelector('button');
const div = document.querySelector('div');

button.addEventListener('click', function() {

Then there's a bit of JavaScript to add a class to the border when the button is clicked. We'll use that class to show the border.

The border set up

To do this we're going to use pseudo-elements. One will be the top and right border, the other will be the bottom and left border. Our animation is going to go from the top left, across, down, across and back up to the top left.

div::after {
  content: '';
  position: absolute;
  width: 0;
  height: 0;
  z-index: -1;

div::before {
  top: calc(var(--padding) * -1 - var(--border-thickness));
  left: calc(var(--padding) * -1);
  border-top: 0px solid var(--border-colour);
  border-right: 0px solid var(--border-colour);

div::after {
  right: calc(var(--padding) * -1);
  bottom: calc(var(--padding) * -1 - var(--border-thickness));
  border-bottom: 0px solid var(--border-colour);
  border-left: 0px solid var(--border-colour);

There's a lot going on here, so let's go through it.

Both sets of borders start off with no width or height, so we can't see them. And a z-index of -1 so if you have text or anything you can interact with in there, the pseudo-elements won't be on top of it.

Both pseudo-elements contain a border - because otherwise they've cover the whole of the div. For now, those borders are 0px in width, as otherwise you'd see squares in the top left and bottom right corners: even though they have no width and height, the border is still visible.

There's also some positioning going on. Since I have padding around the div, I want the borders to be at the edge of that padding, so I have moved them accordingly. Otherwise the elements will ignore the padding and sit at the edge of the background.

Adding the border

When the class of border is added, all we have to do is to set the size and border-width of both pseudo-elements:

div.border::after {
  width: calc(100% + var(--padding) * 2);
  height: calc(100% + var(--padding) * 2 + var(--border-thickness));
  border-width: var(--border-thickness);

Of course this just adds the border instantly. We also need to transition them so we can see them animate:

div::before {
  /* ... */
    width var(--border-transition-time) linear,
    height var(--border-transition-time) linear var(--border-transition-time);

div::after {
  /* ... */
    width var(--border-transition-time) linear calc(var(--border-transition-time) * 2),
    height var(--border-transition-time) linear calc(var(--border-transition-time) * 3),
    border-width 0s linear calc(var(--border-transition-time) * 2);

These are a bit lengthy, so let's go through them.

First we want to see the border going across the top. So we transition the width of the before pseudo-element in --border-transition-time.

Then we want to see the border going down the right side. So we transition the height of the before pseudo-element in --border-transition-time. Except we don't want it to start until it's finished the top border. So we add a delay of --border-transition-time.

After that we want to see the border going along the bottom. Since we want that to wait until we've done the top and right, we need to delay for twice --border-transition-time.

Finally we want the border going up the left, so now we need to wait for three times --border-transition-time.

And we also need to delay the border-width changing from 0px to --border-thickness otherwise as soon as the top border starts animating we see a square of the after pseudo-elements border.

You can always change these times and delays to make the border animation less linear (as well as changing the transition timing function). You just need to be careful to keep track of what is happening when so nothing happens sooner or later than you want it to.

The finished code

Here is the final code: