---
name: gradient-border-cards
description: Use when you want "Modern, premium, polished" - Adds gradient outlines around cards or panels.
---

# Gradient Border Cards

> **Category:** Premium / Futuristic  -  **Personality:** Modern, premium, polished
>
> **Best use:** Pricing, feature cards, SaaS
>
> **Ratings:** Professional 5/5 - Casual 3/5 - Exotic 3/5 - Visual impact 4/5 - Difficulty 2/5 - Performance cost 2/5

## What it is

A set of cards on a dark surface, each wrapped in a **vivid gradient outline** instead of a flat 1px border. The signature interaction: as the pointer moves across the group, a bright highlight travels along the rims — **the border lights up toward the cursor** — while a soft spotlight follows underneath. The "most popular" card carries a slowly rotating conic rim so it reads as the hero. It's the modern pricing/feature look used by AI and developer-tool landing pages; reduced-motion users get the same gradient rims, just static.

## Dependencies / CDN

**None — pure CSS for the rims; ~25 lines of vanilla JS** for the pointer tracking and billing toggle. The demo loads three Google Fonts (optional):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&family=Space+Mono:wght@400;700&family=Unbounded:wght@600;700;800&display=swap" rel="stylesheet">
```

`Unbounded` = display (headings, prices), `Manrope` = body, `Space Mono` = labels/meta.

## HTML

One card. The two empty `<span>`s are the inner spotlight; the rims themselves are CSS pseudo-elements (no markup). The grid wrapper is what drives the "lights toward the pointer" group behaviour.

```html
<div class="gbx-cards">
  <article class="gbx-card is-featured">
    <span class="gbx-badge">Most popular</span>
    <span class="gbx-spot" aria-hidden="true"></span>   <!-- inner cursor spotlight -->
    <div class="gbx-body">
      <div class="gbx-plan">Scale</div>
      <div class="gbx-price"><span class="gbx-amt">$49</span><span class="gbx-per">/mo</span></div>
      <button class="gbx-cta" type="button">Start 14-day trial</button>
      <ul class="gbx-feats"><li>100 requests / second</li><li>Agent memory &amp; vector store</li></ul>
    </div>
  </article>
  <!-- repeat .gbx-card (without .is-featured) for the other tiers -->
</div>
```

## CSS

The whole effect is the masked-ring trick run **twice** — once for the always-on rim, once for a pointer-tracked highlight — plus an inner glow.

```css
.gbx-card{
  --mx:50%; --my:-20%;              /* pointer position, set by JS */
  --gbx-bw:1.4px;                   /* border width */
  --gbx-rim:linear-gradient(150deg,rgba(255,255,255,.30),rgba(168,85,247,.45) 42%,rgba(34,211,238,.30));
  position:relative; border-radius:22px; padding:28px 26px; isolation:isolate;
  background:linear-gradient(180deg,#0e0c1c,#0a0913);
}

/* 1) base vivid rim — a gradient clipped to the rounded border ring */
.gbx-card::before{
  content:""; position:absolute; inset:0; z-index:1; border-radius:inherit; padding:var(--gbx-bw);
  background:var(--gbx-rim); opacity:.85;
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude;   /* keep only the ring */
  pointer-events:none;
}

/* 2) pointer-tracked rim — a white-hot radial centred on the cursor, same ring mask */
.gbx-card::after{
  content:""; position:absolute; inset:0; z-index:1; border-radius:inherit; padding:var(--gbx-bw);
  background:radial-gradient(190px circle at var(--mx) var(--my),
     #fff 0%,#e9d5ff 14%,#a855f7 34%,rgba(168,85,247,0) 62%);
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude;
  opacity:0; transition:opacity .35s ease; pointer-events:none;
}
.gbx-cards:hover .gbx-card::after{opacity:1;}   /* light every rim while the pointer is in the grid */

/* 3) soft inner spotlight that also follows the cursor */
.gbx-spot{
  position:absolute; inset:0; z-index:0; border-radius:inherit; pointer-events:none;
  background:radial-gradient(420px circle at var(--mx) var(--my),rgba(129,140,248,.16),transparent 58%);
  opacity:0; transition:opacity .35s ease;
}
.gbx-cards:hover .gbx-spot{opacity:1;}
.gbx-body{position:relative; z-index:2;}        /* content sits above the rim/glow layers */

/* featured card: brighter, slowly-rotating conic rim (needs @property to animate) */
@property --gbx-spin{syntax:'<angle>'; inherits:false; initial-value:0deg;}
.gbx-card.is-featured::before{
  background:conic-gradient(from var(--gbx-spin,0deg),#22d3ee,#818cf8,#e879f9,#22d3ee);
  opacity:1; animation:gbx-rotate 6s linear infinite;
}
@keyframes gbx-rotate{to{--gbx-spin:360deg}}

/* reduced motion → static rims: no pointer highlight, no spin */
@media (prefers-reduced-motion: reduce){
  .gbx-cards:hover .gbx-card::after,
  .gbx-cards:hover .gbx-spot{opacity:0;}
  .gbx-card.is-featured::before{animation:none;}
}
```

## JavaScript

Per-card pointer position written into `--mx`/`--my`. One listener on the grid; rect reads batched into a `requestAnimationFrame`. Skipped entirely under reduced motion so the rims stay static.

```js
var grid=document.querySelector('.gbx-cards');
var cards=[].slice.call(grid.querySelectorAll('.gbx-card'));
if(!matchMedia('(prefers-reduced-motion: reduce)').matches){
  var raf=0,px=0,py=0;
  function paint(){
    raf=0;
    cards.forEach(function(c){
      var r=c.getBoundingClientRect();
      c.style.setProperty('--mx',(px-r.left).toFixed(1)+'px');
      c.style.setProperty('--my',(py-r.top).toFixed(1)+'px');
    });
  }
  grid.addEventListener('pointermove',function(e){
    px=e.clientX; py=e.clientY;
    if(!raf) raf=requestAnimationFrame(paint);
  });
}
```

## How it works

- **The ring mask.** A pseudo-element fills the whole card with a gradient, then two stacked masks — one clipped to the *content box*, one to the *border box* — are subtracted with `mask-composite: exclude`. Only the `padding`-width ring survives, so the gradient shows exactly as the border.
- **Two rims, one mask.** `::before` is the calm always-on outline; `::after` is the same ring filled with a radial-gradient centred at `var(--mx) var(--my)`. Where the radial is bright (near the cursor) the rim glows white; where it fades to transparent the base rim shows through — so the light appears to slide along the edge toward the pointer.
- **It's a *set*.** Because every card reads the same global pointer (each converted to its own local `--mx/--my`), the highlight flows from card to card as you sweep across the row. `.gbx-cards:hover` fades all `::after` rims in together; cards far from the cursor simply have their radial centred off-card, so they stay dark.
- **Featured spin.** A registered `@property --gbx-spin` angle is animated 0→360°, rotating a `conic-gradient` rim. Registration is what makes the angle interpolate; the `var(--gbx-spin,0deg)` fallback keeps the rim valid (just static) where `@property` is unsupported.

## Customization

- **Border width:** the `--gbx-bw` padding (1–2px is crisp; 3px+ reads as a thick frame).
- **Palette:** swap the stops in `--gbx-rim` and the `::after` radial. Keep a near-white core in the radial — that white-hot point is what sells "light".
- **Highlight reach:** the `190px` radius in `::after` controls how much rim lights up; the `420px` in `.gbx-spot` controls the inner wash.
- **Calmer version:** drop the conic animation and leave only `::before` + `::after` for a clean pointer-reactive border with no motion.
- **Always-lit card:** raise a card's `::before` opacity (or give it the conic rim) to flag the recommended tier without JS.

## Accessibility & performance

- **Reduced motion:** gate the JS with `matchMedia('(prefers-reduced-motion: reduce)')` and hide the hover-driven `::after`/spot in CSS. The gradient rims remain as a fully static design — nothing essential is lost.
- **Cheap:** no libraries, no canvas. JS only writes two custom properties per card, batched in one `rAF`; the browser repaints the masked layers on the GPU. Keep it to a handful of cards.
- **Contrast:** the rim is decoration — never the only signal. Real focus states (`:focus-visible` outlines on the buttons/toggle) and text contrast must stand on their own.
- Mark the glow `<span>`s `aria-hidden="true"`; they carry no meaning.

## Gotchas

- **Both `-webkit-mask-composite:xor` and `mask-composite:exclude` are required** — Safari/Chrome use different keywords for the same operation. Omit one and the rim becomes a solid filled rectangle.
- **The conic rim only animates with `@property`.** Without it the angle can't interpolate (the card just shows a static conic) — and if you drop the `,0deg` fallback in `var()`, an unregistered property makes the whole `background` invalid and the rim disappears.
- **Stacking order matters:** give `.gbx-body` a higher `z-index` than the rim/spot layers, or the glow washes over your text.
- **`overflow:hidden` on the card clips the "Most popular" badge** that sits at `top:-11px`. Let the card overflow and clip at the section/stage instead.
- Set a sensible default for `--mx/--my` (e.g. `50% -20%`) so the rim looks right before the first `pointermove` and on touch devices, where there is no hover.
