CSS & layout

Animating border-image finally works if you register the gradient stop

Animating border-image finally works if you register the gradient stop

You wanted a decorative border that moved. Options were an absolutely-positioned pseudo-element sitting on top of the box, an SVG rim with stroke-dashoffset, or a wrapper div holding a gradient background clipped to a thin strip. All three ship extra nodes to solve one problem.

Preethi's piece on CSS-Tricks pulls the trick back into the border itself. Put a gradient on border-image-source, move one variable inside that gradient, and the border draws itself.

The catch is the same catch that stops every "just animate the gradient" attempt: browsers do not interpolate raw gradient stops. You have to teach them the value type first.

Four properties, one strip

The border-image family does the wiring. -source takes the gradient. -slice picks how the image is cut into the four sides and four corners. -width sets how thick the border strip renders. -outset pushes the strip past the box edge if you want it sitting outside the border-box.

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

border-image-slice: 1 is the pattern to remember. A single slice runs the image across the whole border ring, the same way background-size and background-position map a single background across a box. That is the analogy Preethi reaches for, and it lands.

Why the gradient will not animate on its own

Here is where most experiments stall. You put --p in a gradient stop, transition --p, nothing moves. --p is a plain custom property. The browser has no idea whether it holds a length, a color, an angle or a URL, so it will not interpolate it. It flips at 50%. Not what you want.

The fix is @property.

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

Now --p is a typed custom property. <percentage> is a real CSS value type, and typed values interpolate. Move --p from 0% to 100% and the browser fills in every frame in between. The gradient inside border-image-source redraws each one.

The animation itself is a :hover toggle in the source:

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

A transition on --p (or on the whole element) carries it there. One typed property, one animatable stop.

The conic variant

A linear gradient sliding across a border is one shape. A conic gradient rotating around one is another. Preethi wires that up with two registered properties instead of one.

@property --n {
  syntax: "<number>";
  initial-value: 1;
  inherits: false;
}

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

Then the border draws from a conic gradient reading both:

border-image-source: conic-gradient(from var(--a), red var(--a), transparent 0%);
border-image-repeat: round;
transition-property: --n, --a;
transition-duration: 0.6s;

transition-property names the custom properties directly. That is the modern read of custom-property animation: register the type, then transition the identifier by name.

The corner the border cannot round

Read this before you commit to the layout. Border images do not curve to border shapes. Your border-radius shapes the box, but the border image keeps running along a square ring. On a rounded card the animated rim sits behind the corner curve and clips.

The article calls it out directly: any animation that needs to travel smoothly around a rounded corner is the one this technique cannot do. Hard corner? You are fine. Pill button? Pick a different tool.

Preethi points at two of them. Andy Clarke's recent piece revisits border-image from a design angle, and Temani Afif's approach builds the same visual with CSS mask instead. Same effect, different mechanism, and the mask route follows the corner.

What to try on your next component

Open a component with a decorative outline — a card, a callout, a hover state on a CTA — and follow the shape:

  1. Move the outline into border-image-source as a gradient.
  2. Register one custom property with @property for the stop you want to move.
  3. Transition that property on hover, focus, or a state class.

Count the extra elements you dropped. That number is the point. And when you hit the corner problem, remember it is the corner, not the technique, and reach for a mask.

Source: CSS-Tricks (css-tricks.com)

Related
CSS & layout

Hard color stops turn conic-gradient() into a triangle machine

Chris Coyier makes the case that conic-gradient() with two hard color stops is the fastest way to draw a triangle in CSS. The from and at keywords do most of the work.

July 29, 2026
CSS & layout

Opposing scroll columns with one view() timeline and three keyframe sets

CSS-Tricks walks through making adjacent columns drift in opposite directions as the page scrolls, with no JavaScript. The trick is a per-item animation-timeline: view() and animation-range: entry 0% cover 100%, repeated across three keyframe sets.

June 25, 2026
CSS & layout

Scroll-triggered animations land in CSS: `timeline-trigger` is not `animation-timeline`

Chrome 146 is the first browser to ship CSS scroll-triggered animations. The new `timeline-trigger` property fires a fixed-duration animation when a scroll threshold is crossed, and it is a close cousin of scroll-driven animations on a very different mechanism.

June 23, 2026