---
name: gradient-border-card
description: Use when you want "Premium, modern, clean" - Applies a gradient outline around a card.
---

# Gradient Border Card

> **Category:** Card / Layout  -  **Personality:** Premium, modern, clean
>
> **Best use:** Pricing cards, highlighted features
>
> **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 gradient border card wraps a card in a **multi-colour outline** instead of a flat 1px line, so a single tier or feature reads as "special" without changing its size or layout. This recipe uses a gradient-filled `::before` pseudo-element that is masked down to just the border ring (`mask-composite: exclude`), which—unlike `border-image`—honours `border-radius` perfectly. The border itself is **static** (no rotation/glow loop), so it stays calm and premium; the highlighted plan simply gets a brighter, thicker version of the same gradient. It is the standard way to mark the "Most popular" plan in a pricing table or to frame a featured feature card.

## Dependencies / CDN

None — pure CSS for the effect. The demo loads two display/body 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=Hanken+Grotesk:wght@400;500;600;700&family=Sora:wght@600;700;800&display=swap" rel="stylesheet">
```

The billing toggle (Monthly / Annual) is a tiny bit of vanilla JS and is **not** part of the border effect.

## HTML

A card is just a container; one variant class (`is-featured`) swaps in the vivid gradient:

```html
<article class="gbc-card">
  <div class="gbc-plan">Indie</div>
  <p class="gbc-tag">For solo builders</p>
  <div class="gbc-price"><span class="gbc-amt">$0</span><span class="gbc-per">forever</span></div>
  <button class="gbc-cta">Start for free</button>
  <ul class="gbc-feats"><li>…</li></ul>
</article>

<article class="gbc-card is-featured">
  <span class="gbc-badge">Most popular</span>
  …
</article>
```

## CSS

```css
.gbc-card{
  /* per-card knobs: the ring gradient + its thickness */
  --gbc-ring: linear-gradient(160deg,
              rgba(255,255,255,.24), rgba(255,255,255,.05) 52%, rgba(120,200,255,.16));
  --gbc-bw: 1px;
  position: relative;
  border-radius: 22px;
  padding: 30px 26px;
  background: linear-gradient(180deg,#11131d,#0d0e16);   /* opaque card surface */
}

/* THE EFFECT — a gradient rectangle, masked down to the border ring */
.gbc-card::before{
  content:"";
  position:absolute; inset:0;
  border-radius:inherit;
  padding: var(--gbc-bw);                 /* ring thickness */
  background: var(--gbc-ring);            /* the gradient */
  /* paint two masks: full box + content box, then keep only the difference */
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
          mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;            /* old WebKit keyword */
          mask-composite: exclude;        /* standard */
  pointer-events:none;
}

/* the highlighted plan: same mechanism, brighter + thicker ring */
.gbc-card.is-featured{
  --gbc-ring: linear-gradient(120deg,#ff8fb1 0%,#b39bff 48%,#66d6ff 100%);
  --gbc-bw: 1.6px;
  box-shadow: 0 40px 90px -42px rgba(150,120,255,.55);   /* soft brand glow */
}
```

## JavaScript

Not required for the border. The demo's optional billing toggle just swaps price text and the "billed monthly/annually" note:

```js
var toggle = document.querySelector('.gbc-toggle');
var opts   = toggle.querySelectorAll('.gbc-opt');
var amts   = document.querySelectorAll('.gbc-amt[data-m]');   // data-m / data-a hold the prices
function setPeriod(p){
  toggle.setAttribute('data-period', p);                      // CSS slides the knob
  opts.forEach(o => o.setAttribute('aria-pressed', o.dataset.period === p));
  amts.forEach(a => a.textContent = (p === 'annual' ? a.dataset.a : a.dataset.m));
}
opts.forEach(o => o.addEventListener('click', () => setPeriod(o.dataset.period)));
```

## How it works

- The `::before` is sized to the whole card (`inset:0`) and filled with the gradient. On its own it would cover the entire card.
- Two **mask layers** are stacked: one clipped to `content-box` (the area inside the `padding`) and one covering the full box. `mask-composite: exclude` keeps only where they *differ* — i.e. the `padding`-wide strip around the edge. That strip is the border.
- Because `padding` is the only thing defining the ring's thickness, the gradient bends perfectly around the `border-radius`. `border-image` cannot do rounded corners; this can.
- The card's own opaque `background` sits *under* the ring, so content never overlaps the gradient.
- Highlighting is just a CSS variable swap (`--gbc-ring`, `--gbc-bw`) on `.is-featured` — no extra markup.

## Customization

- **Thickness:** bump `--gbc-bw` (1px hairline → 3px statement border).
- **Gradient:** redefine `--gbc-ring` — a 2–3 stop `linear-gradient`, a `conic-gradient` for a rainbow rim, or `radial-gradient` for a corner glow.
- **Subtle vs. loud:** non-featured cards here use a near-white-to-faint-cyan gradient (reads as a lit edge); the featured card uses the full brand gradient. One gradient variable drives both.
- **Simpler alternative (no mask):** `border:2px solid transparent; background: linear-gradient(#0d0e16,#0d0e16) padding-box, linear-gradient(120deg,#ff8fb1,#66d6ff) border-box;` — fewer lines, but you lose independent control of the ring and it can't sit *over* a transparent card.

## Accessibility & performance

- Cheap to paint (one gradient + a static mask, no animation), so it scales to a full grid of cards without cost.
- Don't rely on the coloured border alone to signal the recommended plan — keep the visible **"Most popular"** badge for colour-blind and low-vision users.
- Keep real text contrast on the card *surface*, not the rim; the border is decorative (`pointer-events:none`, no ARIA).
- The hover lift and toggle knob are `transform`/`opacity` transitions and are disabled under `@media (prefers-reduced-motion: reduce)`.

## Gotchas

- **`mask-composite` needs both spellings:** `-webkit-mask-composite: xor` for older WebKit/Blink and `mask-composite: exclude` for the standard. Ship both `-webkit-mask` and `mask` too, or Firefox shows a filled rectangle instead of a ring.
- **The card needs an opaque background.** If the surface is transparent, the gradient fill shows through the middle (the mask only clips the pseudo, not the card body).
- **`box-sizing` matters:** with `border-box`, `padding` on the `::before` equals the ring width exactly — that's the intent. A stray `border` on the pseudo would stack on top of the padding ring.
- This is a **static** border by design. To make the gradient travel/rotate, you'd animate a `conic-gradient` angle (via `@property --angle`) — but that's the separate "Animated Border Glow" effect, not this one.
