A Different Way to Think About Borders

The border-image property is often relegated to decorative tricks, but it’s also a powerful tool for dynamic UI effects. Because it treats a border as a canvas for any image or gradient, it opens up possibilities that plain solid or dashed lines simply can't match. One compelling use case is animating the border image itself, turning a static frame into a living part of the interface.

There's a well-known limitation: border images don't follow the curvature of border-radius. That makes animating a border around rounded corners tricky. However, for elements with square or rectangular shapes, the effect is direct and visually striking.

The efficiency of border-image is a key advantage. It automatically replicates a design across all four sides, and the border-image-slice property gives you precise control over how much of the source image is used, similar to how background-size works. This makes it straightforward to create complex, cohesive borders without repeating background logic.

Setting Up the Stage

We start with a simple div containing formatted text:

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

The visual foundation is set with fixed dimensions and a background image:

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

To keep the text from sitting directly on the background, a bit of padding is applied.

For the border itself, we'll use a CSS gradient. It's a type of background image, and its behavior makes it ideal for demonstrating the animation principles. We'll define its longhand properties individually for clarity.

A basic linear-gradient produces the initial animated effect:

.card {
  /* ... */

  /* Creates a linear color */
  border-image-source: linear-gradient(-45deg, red 0%, transparent 0%);
  
  /* Controls how the gradient is carved */
  border-image-slice: 1;

  /* Sets the physical thickness of the border */
  border-image-width: 5px;
}

You might notice a small gap between the animated border and the card's background. The border-image-outset property pushes the border away from the element's edge to fix this:

.card {
  /* ... */

  border-image-source: linear-gradient(-45deg, red 0%, transparent 0%);
  border-image-slice: 1;
  border-image-width: 5px;

  /* Pushes the border away from the div */
  border-image-outset: 5px;
}

To render the border, the border-image-source and border-image-width properties are essential. The gradient starts fully transparent. By placing both the red and transparent colors at 0%, there is no space for blending, and the transparent color (appearing later in the list) takes over the entire gradient from the start. Using border-image-slice: 1 creates 1px slices, resulting in a smooth, uniform fill across the border.

The Core Challenge: Animating Gradients

Standard CSS can't transition gradients directly because it involves interpolating the percentage-based positions of color stops. However, we can work around this by registering a custom property and animating that:

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

This registered variable is then used in the gradient:

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

The animation is triggered on hover, transitioning the property --p from its default 0% to 100%:

.card {
  /* ... */
  border-image-source: linear-gradient(-45deg, red var(--p), transparent 0%);
  /* ... */

  &:hover {
    --p: 100%;
  }
}

This instructs the browser to animate the red color's final position all the way to the end. The result is a gradient that expands into a solid red, creating the illusion of the border drawing itself.

Beyond Linear: Conic Gradients

The same concept applies to other gradient types. For example, a conic-gradient introduces a different visual language. This approach animates both the border-image-slice value (using a registered property --n) and the angle of the conic gradient (using --a). The border-image-repeat: round property is critical here, as it tiles the slices to fit a whole number within the border, stretching or squeezing them as necessary for a perfect fit.

.card {
  /* Creates a sharp, expanding color wheel using the custom angle variable */
  border-image-source: conic-gradient(from 0deg, red 0deg, transparent 0%);

  /* Repeats the sliced regions smoothly without clipping */
  border-image-repeat: round;

  border-image-width: 5px;
  border-image-slice: 1;
}

Both custom properties are registered to allow for smooth transitions:

/* Registers a custom property for the border-image-slice */
@property --n {
  syntax: "<number>";
  initial-value: 1;
  inherits: false;
}

/* Registers a custom property for the gradient angle */
@property --a {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

Note: It's possible to animate border-image-slice directly, but the visual result can vary between browsers. Registering a custom property offers more consistent control.

These properties are integrated into the main stylesheet:

.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;
}

Specifying exactly which properties to transition is beneficial for performance:

.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;
}

The transition is then applied on hover:

.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;

  &:hover {
    /* Increases slicing depth to repeat/tile the border pattern */
    --n: 20;
    /* Rotates the gradient full circle to draw the border in */
    --a: 360deg;
  }
}

The slice value (--n) starts at 1 and increases to 20 on hover, making the slices up to 20px thick. This change is visually apparent as breaks and patterns appear in the border, creating a striking, lively effect. This approach demonstrates that border-image isn’t just for static styling—it’s a canvas for motion and a robust way to build engaging interfaces.