A gradient that travels around a card or button border is a common way to highlight a pricing plan, a "recommended" badge or a primary action. border-image ignores border-radius, so in practice the effect is built from backgrounds or pseudo-elements. This guide covers three techniques, each with CSS you can paste in, and compares the tradeoffs. The examples assume a Tailwind CSS v4.3 project, but the CSS itself does not depend on Tailwind.
Browser support data checked on 2026-10-14.
Technique 1: rotate a conic-gradient layer
Give the outer container 2px of padding and clip its overflow. Behind the content, place a square larger than the container, fill it with a conic-gradient, and spin it with transform: rotate(). The inner content covers the middle, and the 2px that remains visible is the border.
.spin-border {
position: relative;
overflow: hidden;
border-radius: 12px;
padding: 2px;
}
.spin-border::before {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 200%;
aspect-ratio: 1;
transform: translate(-50%, -50%);
background: conic-gradient(from 0deg, transparent 0 70%, #6366f1, #ec4899);
animation: spin-border 4s linear infinite;
}
.spin-border > * {
position: relative;
border-radius: 10px;
background: #09090b;
}
@keyframes spin-border {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.spin-border::before {
animation: none;
}
}The same structure with Tailwind utilities:
<div className="relative overflow-hidden rounded-xl p-[2px]">
<div
aria-hidden="true"
className="pointer-events-none absolute left-1/2 top-1/2 aspect-square w-[200%] -translate-x-1/2 -translate-y-1/2 animate-[spin_4s_linear_infinite] bg-[conic-gradient(from_0deg,transparent_0_70%,#6366f1,#ec4899)] motion-reduce:animate-none"
/>
<div className="relative rounded-[calc(var(--radius-xl)-2px)] bg-zinc-950 px-6 py-4 text-zinc-100">
Deployed to production
</div>
</div>In Tailwind v4, -translate-x-1/2 sets the standalone translate property, while the built-in spin keyframes animate transform. They do not overwrite each other, so the stock spin keyframes work as-is.
Tradeoffs: the animated property is transform, which browsers can usually hand to the compositor, so this is the cheapest option. The inner element must have an opaque background, and the rotating square must be large enough to cover the container's diagonal. w-[200%] works for elements wider than they are tall; for tall cards, size the square from the height instead, or the corners show through at some angles. conic-gradient is supported from Chrome 69, Firefox 83 and Safari 12.1 (caniuse), so nothing here needs a recent feature.
Technique 2: animate the gradient angle with @property
Instead of rotating an element, rotate the gradient. Custom properties are not interpolated by default. Registering one as an <angle> with @property lets the browser compute it frame by frame inside keyframes.
@property --border-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.angle-border {
border: 2px solid transparent;
border-radius: 12px;
background:
linear-gradient(var(--card-bg, #09090b), var(--card-bg, #09090b)) padding-box,
conic-gradient(from var(--border-angle), #6366f1, #ec4899, #f59e0b, #6366f1) border-box;
animation: border-angle 4s linear infinite;
}
@keyframes border-angle {
to {
--border-angle: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.angle-border {
animation: none;
}
}The two background layers are clipped to padding-box and border-box. The top solid layer fills the padding area, and the gradient underneath only shows through the transparent border. It takes a single element, with no pseudo-element or extra wrapper.
In a Tailwind v4 project, put @property and @keyframes at the top level of your global CSS. If you want the class to work with variants such as hover: or md:, rewrite the .angle-border block as @utility angle-border { ... }.
Tradeoffs: the background is repainted every frame, which costs more than Technique 1, so watch it when a page has many of these. The inner fill must still be an opaque colour. In exchange, the border width and radius are the element's own border and border-radius, so there is nothing to show through at the corners.
Browser support: MDN marks @property as Baseline 2024, available across all three engines since July 2024: Chrome and Edge 85, Safari 16.4, Firefox 128. On 2026-10-14 caniuse reports about 96.13% global support (caniuse). Browsers without it treat the unregistered variable as a discrete value; 0deg and 360deg look identical, so the result is a static gradient border and the layout is unaffected.
Technique 3: cut out the centre with a mask
Both techniques above require an opaque inner background. If the card is translucent glass, or a background image needs to show through, the centre of the gradient has to be removed. Stack two mask layers on a pseudo-element, one covering only the content-box and one covering the whole box, and composite them with exclude. What remains is the ring of padding.
@property --border-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.mask-border {
position: relative;
border-radius: 12px;
}
.mask-border::before {
content: "";
position: absolute;
inset: 0;
padding: 2px;
border-radius: inherit;
background: conic-gradient(from var(--border-angle), #6366f1, #ec4899, #f59e0b, #6366f1);
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
animation: border-angle 4s linear infinite;
pointer-events: none;
}
@keyframes border-angle {
to {
--border-angle: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.mask-border::before {
animation: none;
}
}Declaration order matters. The mask shorthand resets mask-composite to its initial value, so mask-composite: exclude has to come after mask. The two -webkit- lines are for browsers that only understand the legacy syntax, where xor corresponds to the standard exclude.
Tradeoffs: the centre is fully transparent, so any background shows through; this is the only one of the three that allows it. The cost is the same as Technique 2, a repaint per frame. Border width comes from the pseudo-element's padding. The animation still depends on @property, and falls back to a static border without it. MDN lists mask-composite as available across major browsers since December 2023 (MDN).
Comparison
| Rotating layer | @property angle | Mask cut-out | |
|---|---|---|---|
| Inner background | Must be opaque | Must be opaque | Can be transparent |
| Animated property | transform | Registered custom property | Registered custom property |
| Per-frame cost | Low, usually composited | Background repaint | Pseudo-element repaint |
| DOM needed | Wrapper, spinning layer, content | One element | One element plus a pseudo-element |
| Older browsers | Needs only conic-gradient | Static gradient border | Static gradient border |
One thing all three share: a conic-gradient sweeps an angle around the centre, so on a rectangle the bright spot moves slowly across the middle of the long edges and quickly around the corners. If the highlight needs to travel at a constant speed along the perimeter, move an element along a rounded-rectangle path with offset-path instead. That is how Border Beam, below, works.
Border and button components in WebberUI
These components already handle prefers-reduced-motion (through useReducedMotion from motion/react). They install with the WebberUI CLI and the source goes into your project:
- Border Beam: a beam of light that travels around a container's border. It uses
offset-path: rect(... round ...)to follow the rounded corners andmask-compositeto show only the border area. Free. - Moving Border Button: a point of light circling a button's rounded border, also positioned with
offset-path. Free. - Neon Gradient Card: a rotating conic gradient border with an outer glow. It updates the gradient angle each frame instead of rotating an element, which avoids the exposed corners of Technique 1. Free.
- Shimmer Button: light flowing around the button edge, with an inner highlight and hover depth. Free.
- Rainbow Button: a flowing rainbow gradient button in solid and outline styles. Free.
- Electric Border: an SVG
feTurbulencedisplacement that distorts the stroke into a jittering electric border. Free.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add border-beam.ennpx webberui@latest add https://webberui.com/r/border-beam.en.jsonSee more in the cards and grids and buttons and toggles categories. If the border sits inside a scrolling strip, see building an infinite marquee with Tailwind.