Home / Articles / Color & Theming /

colorgradients

Gradient borders

Three proven techniques for adding gradient borders to elements — each with different tradeoffs for border-radius, flexibility, and browser support.

Technique 1: border-image

The simplest approach uses border-image with a gradient as the image source. It works in a single declaration and requires no pseudo-elements or extra markup.

/* border-image gradient — simplest method */
.card-border-image {
  border: 2px solid;
  border-image: linear-gradient(
    135deg,
    oklch(0.65 0.22 265),
    oklch(0.60 0.20 300)
  ) 1;
  padding: 1.5rem;
}

/* With more color stops */
.card-rainbow {
  border: 3px solid;
  border-image: linear-gradient(
    90deg,
    oklch(0.6 0.2 25),
    oklch(0.7 0.17 55),
    oklch(0.6 0.2 145),
    oklch(0.6 0.2 265),
    oklch(0.55 0.2 300)
  ) 1;
}

The 1 at the end is the border-image-slice value. For gradients, 1 is almost always correct — it uses the entire gradient as the border.

Limitation: border-image does not work with border-radius. The corners will always be square. If you need rounded corners, use one of the other techniques.

Technique 2: background-clip double layer

This technique layers two backgrounds — a solid color clipped to the content area, and a gradient that fills the padding area. The gradient peeks through in the border zone.

/* background-clip trick — works with border-radius */
.card-clip {
  /* The "border" width is controlled by padding */
  padding: 2px;
  border-radius: 0.75rem;
  background: linear-gradient(
    135deg,
    oklch(0.65 0.22 265),
    oklch(0.60 0.20 300)
  );

  /* Inner content wrapper needed */
  /* The inner element covers the gradient, leaving the edges visible */
}

.card-clip__inner {
  background: oklch(0.19 0.02 260);
  border-radius: calc(0.75rem - 2px);
  padding: 1.5rem;
}

/* Alternative: using background-clip on a single element */
.card-clip-single {
  border: 2px solid transparent;
  border-radius: 0.75rem;
  background:
    linear-gradient(oklch(0.19 0.02 260), oklch(0.19 0.02 260)) padding-box,
    linear-gradient(135deg, oklch(0.65 0.22 265), oklch(0.60 0.20 300)) border-box;
}

The single-element version uses two comma-separated backgrounds: the first is a solid color clipped to padding-box, the second is the gradient filling the border-box. The transparent border lets the gradient show through.

Technique 3: mask-based border

For the most flexibility, you can use mask with a pseudo-element. The pseudo-element holds the gradient, and a mask hides everything except the border ring.

/* Mask-based gradient border */
.card-mask {
  position: relative;
  border-radius: 0.75rem;
  padding: 1.5rem;
  background: oklch(0.19 0.02 260);
}

.card-mask::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 2px;  /* border thickness */
  background: linear-gradient(
    135deg,
    oklch(0.65 0.22 265),
    oklch(0.60 0.20 300)
  );
  mask:
    linear-gradient(oklch(0 0 0) 0 0) content-box,
    linear-gradient(oklch(0 0 0) 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

The mask trick works by creating two opaque layers — one covering the content area and one covering the entire element — then excluding the content layer from the full layer. What remains is just the border ring, which reveals the gradient underneath.

Comparing the three techniques

Each approach has distinct strengths. Choose based on your specific requirements:

/*
  Technique        | border-radius | Single element | Hover animation
  -----------------+---------------+----------------+----------------
  border-image     | No            | Yes            | Yes
  background-clip  | Yes           | Yes*           | Limited
  mask composite   | Yes           | Pseudo-element | Yes

  * background-clip single-element version requires matching
    the inner background color to the page background.

  Recommendation:
  - No rounded corners needed → border-image (simplest)
  - Rounded corners, known background → background-clip
  - Rounded corners, any background → mask composite
*/

/* All three producing visually similar results: */

/* 1. border-image */
.demo-a {
  border: 2px solid;
  border-image: linear-gradient(135deg, oklch(0.65 0.22 265), oklch(0.6 0.2 300)) 1;
}

/* 2. background-clip */
.demo-b {
  border: 2px solid transparent;
  border-radius: 0.75rem;
  background:
    linear-gradient(oklch(0.19 0.02 260), oklch(0.19 0.02 260)) padding-box,
    linear-gradient(135deg, oklch(0.65 0.22 265), oklch(0.6 0.2 300)) border-box;
}

/* 3. mask composite (see previous section for full code) */

Animating gradient borders

Gradient borders become more engaging when animated. The @property rule lets you animate individual color stops or angles within the gradient.

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

.card-animated {
  border: 2px solid transparent;
  border-radius: 0.75rem;
  background:
    linear-gradient(oklch(0.19 0.02 260), oklch(0.19 0.02 260)) padding-box,
    linear-gradient(
      var(--border-angle),
      oklch(0.65 0.22 265),
      oklch(0.60 0.20 300),
      oklch(0.65 0.22 265)
    ) border-box;
  animation: spin-border 4s linear infinite;
}

@keyframes spin-border {
  to {
    --border-angle: 495deg; /* 135 + 360 */
  }
}

/* The gradient appears to rotate around the card continuously */

Without @property, the angle would snap from 135deg to 495deg. Registering it as an <angle> type enables smooth interpolation.

Gradient borders with hover effects

A common UI pattern is showing a gradient border only on hover, or intensifying it on interaction. Here is a clean implementation using the background-clip technique:

@property --border-opacity {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

.card-hover {
  border: 2px solid transparent;
  border-radius: 0.75rem;
  background:
    linear-gradient(oklch(0.19 0.02 260), oklch(0.19 0.02 260)) padding-box,
    linear-gradient(
      135deg,
      oklch(0.65 0.22 265 / var(--border-opacity)),
      oklch(0.60 0.20 300 / var(--border-opacity))
    ) border-box;
  transition: --border-opacity 0.3s ease;
}

.card-hover:hover {
  --border-opacity: 1;
}

/* Subtle default border visible at rest */
.card-hover-subtle {
  --border-opacity: 0.3;
}

.card-hover-subtle:hover {
  --border-opacity: 1;
}

This technique fades the gradient border in and out smoothly. The @property registration for the opacity value ensures the transition interpolates correctly rather than snapping.