Dev & EngARTICLE

Animating border-image in CSS: borders that draw themselves

I took the technique from CSS-Tricks, ran it in a sandbox and measured the result: you can animate border gradients with @property and transition, without JavaScript.

Animating border-image in CSS: borders that draw themselves
Image: Carina Ferreira

The border-image property is one of those that we tend to ignore. It has been around for years and lets you use an image or gradient as a border instead of a plain solid or dashed one. What caught my interest in Preethi's article on CSS-Tricks was taking it further: animating that border to create the effect of a line drawing itself around the element. I reproduced everything in a CodePen and I'm bringing here what worked (and what broke along the way).

Why border-image and not a mask

First, the honest caveat: border-image does not follow border-radius. If your card has rounded corners, the animated border will ignore that. There's Temani Afif's approach using mask, which respects the curvature, and it's great. I chose border-image because it's efficient at replicating the same design across all four borders automatically and because border-image-slice opens up effects that are hard to achieve any other way. It's a trade-off, not a silver bullet.

The basics

The HTML is a simple div:

html
<div class="card"><strong>Bruce Wayne</strong></div>

And the base of the card, with a background image:

css
.card {
  width: 150px;
  aspect-ratio: 0.69;
  position: relative;
  background: center/90% no-repeat;
  background-image: url("batman.jpg");
}

Now the border. I used a single-color gradient because a gradient is a type of image and it's easier to demonstrate:

css
.card {
  border-image-source: linear-gradient(-45deg, red 0%, transparent 0%);
  border-image-slice: 1;   /* 1px slice, fills the whole border */
  border-image-width: 5px; /* physical border thickness */
  border-image-outset: 5px; /* pushes the border away from the card */
}

I used the longhand properties on purpose, so it's clear which value affects what. The outset fixed an annoying gap that appeared between the border and the image. Since both colors (red and transparent) start at 0%, the browser has no room to blend: the border stays fully transparent at the start. It's that 0% we're going to animate.

The trick: @property

This was my first stumble. I tried animating the percentage inside the gradient directly and it didn't work. Gradients don't interpolate by default, because you're transitioning stop points that are percentages, and the browser doesn't know how to interpolate that.

The solution is to register a typed custom property. Only then does it become animatable:

css
@property --p {
  syntax: "<percentage>";
  initial-value: 0%;
  inherits: false;
}

With it inside the gradient and a transition on hover, the border draws itself:

css
.card {
  border-image-source: linear-gradient(-45deg, red var(--p), transparent 0%);
}
.card:hover {
  --p: 100%;
}

The red grows from 0% to 100%, as if the border were being traced. Simple, and without a single line of JavaScript.

Leveling up: rotating conic-gradient

You can go further by swapping the linear-gradient for a conic-gradient and animating two values at the same time: border-image-slice (which I called --n) and the gradient's angle (--a).

css
@property --n {
  syntax: "<number>";
  initial-value: 1;
  inherits: false;
}
@property --a {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

.card {
  border-image-source: conic-gradient(from var(--a), red var(--a), transparent 0%);
  border-image-width: 5px;
  border-image-slice: var(--n);
  border-image-repeat: round;
  transition-property: --n, --a;
  transition-duration: 0.6s;
}
.card:hover {
  --n: 20;
  --a: 360deg;
}

border-image-repeat: round tries to fit a whole number of "tiles" onto the border, stretching or slightly compressing them to fit. The --a rotating 360° makes the border draw itself in a circle. The --n going from 1 to 20 thickens the slices, and you can see the seams appear in the trace.

A note on performance (and accessibility)

Two points I won't leave as a footnote. First: declaring transition-property: --n, --a explicitly, instead of transition: all, keeps the browser from watching properties that don't change. Second, and more important: since the effect depends on :hover, wrap the animation in a @media (prefers-reduced-motion: no-preference) to respect those who asked for less motion. It costs three lines and it's the bare minimum.

Preethi's homework challenge still stands: take these examples apart, test varying direction, speed and gradient types. It's a lot more fun than it sounds.

Translated from the Brazilian Portuguese original · Read the original

Read also