Smooth Gradient Hue-Shift Border Button
The 'Most Popular' plan highlighter: the button's border and halo drift continuously through the color wheel — violet to cyan to rose and back — by animating a single registered hue number through oklch(). Because only hue moves (lightness and chroma pinned), every frame is equally vivid and the label never loses contrast.
Published
The code
<section class="gbb-14" aria-label="Hue shifting gradient border button demo">
<div class="gbb-14__plan">
<span class="gbb-14__badge"><span class="gbb-14__dot" aria-hidden="true"></span>Most Popular</span>
<button class="gbb-14__btn" type="button">Choose Pro — $19/mo</button>
</div>
</section><section class="gbb-14" aria-label="Hue shifting gradient border button demo">
<div class="gbb-14__plan">
<span class="gbb-14__badge"><span class="gbb-14__dot" aria-hidden="true"></span>Most Popular</span>
<button class="gbb-14__btn" type="button">Choose Pro — $19/mo</button>
</div>
</section>.gbb-14,
.gbb-14 *,
.gbb-14 *::before,
.gbb-14 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@property --gbb-h {
syntax: '<number>';
inherits: true;
initial-value: 280;
}
.gbb-14 {
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: oklch(0.14 0.015 280);
animation: gbb14-drift 6s linear infinite;
}
.gbb-14 .gbb-14__plan {
display: grid;
justify-items: center;
gap: 14px;
}
.gbb-14 .gbb-14__badge {
display: inline-flex;
align-items: center;
gap: 7px;
font: 700 11px/1 'Segoe UI',system-ui,sans-serif;
letter-spacing: .09em;
text-transform: uppercase;
color: rgba(255,255,255,.85);
border: 1px solid oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .4);
border-radius: 999px;
padding: 6px 13px;
}
.gbb-14 .gbb-14__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: oklch(0.75 0.19 calc(var(--gbb-h) * 1deg));
box-shadow: 0 0 8px oklch(0.75 0.19 calc(var(--gbb-h) * 1deg));
}
.gbb-14 .gbb-14__btn {
cursor: pointer;
border: 2px solid transparent;
border-radius: 14px;
padding: 16px 32px;
font: 700 15px/1 'Segoe UI',system-ui,sans-serif;
color: #fff;
background: linear-gradient(oklch(0.18 0.02 280),oklch(0.18 0.02 280)) padding-box,linear-gradient(120deg,oklch(0.7 0.19 calc(var(--gbb-h) * 1deg)),oklch(0.7 0.19 calc((var(--gbb-h) + 60) * 1deg))) border-box;
box-shadow: 0 0 22px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .3);
transition: box-shadow .3s ease,transform .25s ease;
}
.gbb-14 .gbb-14__btn:hover,
.gbb-14 .gbb-14__btn:focus-visible {
transform: translateY(-2px);
box-shadow: 0 0 38px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .5),0 0 0 4px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .18);
}
.gbb-14:has(.gbb-14__btn:hover),
.gbb-14:has(.gbb-14__btn:focus-visible) {
animation-duration: 2.5s;
}
.gbb-14 .gbb-14__btn:focus-visible {
outline: 2px solid #fff;
outline-offset: 4px;
}
.gbb-14 .gbb-14__btn:active {
transform: translateY(0) scale(.98);
}
@keyframes gbb14-drift {
from {
--gbb-h: 0;
}
to {
--gbb-h: 360;
}
}
@media (prefers-reduced-motion: reduce) {
.gbb-14 {
animation: none;
}
}.gbb-14,
.gbb-14 *,
.gbb-14 *::before,
.gbb-14 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@property --gbb-h {
syntax: '<number>';
inherits: true;
initial-value: 280;
}
.gbb-14 {
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: oklch(0.14 0.015 280);
animation: gbb14-drift 6s linear infinite;
}
.gbb-14 .gbb-14__plan {
display: grid;
justify-items: center;
gap: 14px;
}
.gbb-14 .gbb-14__badge {
display: inline-flex;
align-items: center;
gap: 7px;
font: 700 11px/1 'Segoe UI',system-ui,sans-serif;
letter-spacing: .09em;
text-transform: uppercase;
color: rgba(255,255,255,.85);
border: 1px solid oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .4);
border-radius: 999px;
padding: 6px 13px;
}
.gbb-14 .gbb-14__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: oklch(0.75 0.19 calc(var(--gbb-h) * 1deg));
box-shadow: 0 0 8px oklch(0.75 0.19 calc(var(--gbb-h) * 1deg));
}
.gbb-14 .gbb-14__btn {
cursor: pointer;
border: 2px solid transparent;
border-radius: 14px;
padding: 16px 32px;
font: 700 15px/1 'Segoe UI',system-ui,sans-serif;
color: #fff;
background: linear-gradient(oklch(0.18 0.02 280),oklch(0.18 0.02 280)) padding-box,linear-gradient(120deg,oklch(0.7 0.19 calc(var(--gbb-h) * 1deg)),oklch(0.7 0.19 calc((var(--gbb-h) + 60) * 1deg))) border-box;
box-shadow: 0 0 22px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .3);
transition: box-shadow .3s ease,transform .25s ease;
}
.gbb-14 .gbb-14__btn:hover,
.gbb-14 .gbb-14__btn:focus-visible {
transform: translateY(-2px);
box-shadow: 0 0 38px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .5),0 0 0 4px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .18);
}
.gbb-14:has(.gbb-14__btn:hover),
.gbb-14:has(.gbb-14__btn:focus-visible) {
animation-duration: 2.5s;
}
.gbb-14 .gbb-14__btn:focus-visible {
outline: 2px solid #fff;
outline-offset: 4px;
}
.gbb-14 .gbb-14__btn:active {
transform: translateY(0) scale(.98);
}
@keyframes gbb14-drift {
from {
--gbb-h: 0;
}
to {
--gbb-h: 360;
}
}
@media (prefers-reduced-motion: reduce) {
.gbb-14 {
animation: none;
}
}/* No JavaScript — one registered number (--gbb-h) animates 0→360; every oklch() color on the button derives its hue from it via calc(). */
/* No JavaScript — one registered number (--gbb-h) animates 0→360; every oklch() color on the button derives its hue from it via calc(). */Here's a working CSS Glowing Border Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Smooth Gradient Hue-Shift Border Button
Source: https://codefronts.com/components/css-glowing-border-buttons/smooth-gradient-hue-shift-border-button/
The 'Most Popular' plan highlighter: the button's border and halo drift continuously through the color wheel — violet to cyan to rose and back — by animating a single registered hue number through oklch(). Because only hue moves (lightness and chroma pinned), every frame is equally vivid and the label never loses contrast.
## HTML
```html
<section class="gbb-14" aria-label="Hue shifting gradient border button demo">
<div class="gbb-14__plan">
<span class="gbb-14__badge"><span class="gbb-14__dot" aria-hidden="true"></span>Most Popular</span>
<button class="gbb-14__btn" type="button">Choose Pro — $19/mo</button>
</div>
</section>
```
## CSS
```css
.gbb-14,
.gbb-14 *,
.gbb-14 *::before,
.gbb-14 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@property --gbb-h {
syntax: '<number>';
inherits: true;
initial-value: 280;
}
.gbb-14 {
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: oklch(0.14 0.015 280);
animation: gbb14-drift 6s linear infinite;
}
.gbb-14 .gbb-14__plan {
display: grid;
justify-items: center;
gap: 14px;
}
.gbb-14 .gbb-14__badge {
display: inline-flex;
align-items: center;
gap: 7px;
font: 700 11px/1 'Segoe UI',system-ui,sans-serif;
letter-spacing: .09em;
text-transform: uppercase;
color: rgba(255,255,255,.85);
border: 1px solid oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .4);
border-radius: 999px;
padding: 6px 13px;
}
.gbb-14 .gbb-14__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: oklch(0.75 0.19 calc(var(--gbb-h) * 1deg));
box-shadow: 0 0 8px oklch(0.75 0.19 calc(var(--gbb-h) * 1deg));
}
.gbb-14 .gbb-14__btn {
cursor: pointer;
border: 2px solid transparent;
border-radius: 14px;
padding: 16px 32px;
font: 700 15px/1 'Segoe UI',system-ui,sans-serif;
color: #fff;
background: linear-gradient(oklch(0.18 0.02 280),oklch(0.18 0.02 280)) padding-box,linear-gradient(120deg,oklch(0.7 0.19 calc(var(--gbb-h) * 1deg)),oklch(0.7 0.19 calc((var(--gbb-h) + 60) * 1deg))) border-box;
box-shadow: 0 0 22px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .3);
transition: box-shadow .3s ease,transform .25s ease;
}
.gbb-14 .gbb-14__btn:hover,
.gbb-14 .gbb-14__btn:focus-visible {
transform: translateY(-2px);
box-shadow: 0 0 38px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .5),0 0 0 4px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .18);
}
.gbb-14:has(.gbb-14__btn:hover),
.gbb-14:has(.gbb-14__btn:focus-visible) {
animation-duration: 2.5s;
}
.gbb-14 .gbb-14__btn:focus-visible {
outline: 2px solid #fff;
outline-offset: 4px;
}
.gbb-14 .gbb-14__btn:active {
transform: translateY(0) scale(.98);
}
@keyframes gbb14-drift {
from {
--gbb-h: 0;
}
to {
--gbb-h: 360;
}
}
@media (prefers-reduced-motion: reduce) {
.gbb-14 {
animation: none;
}
}
```
## JavaScript
```js
/* No JavaScript — one registered number (--gbb-h) animates 0→360; every oklch() color on the button derives its hue from it via calc(). */
```Here's a working CSS Glowing Border Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Smooth Gradient Hue-Shift Border Button
Source: https://codefronts.com/components/css-glowing-border-buttons/smooth-gradient-hue-shift-border-button/
The 'Most Popular' plan highlighter: the button's border and halo drift continuously through the color wheel — violet to cyan to rose and back — by animating a single registered hue number through oklch(). Because only hue moves (lightness and chroma pinned), every frame is equally vivid and the label never loses contrast.
## HTML
```html
<section class="gbb-14" aria-label="Hue shifting gradient border button demo">
<div class="gbb-14__plan">
<span class="gbb-14__badge"><span class="gbb-14__dot" aria-hidden="true"></span>Most Popular</span>
<button class="gbb-14__btn" type="button">Choose Pro — $19/mo</button>
</div>
</section>
```
## CSS
```css
.gbb-14,
.gbb-14 *,
.gbb-14 *::before,
.gbb-14 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@property --gbb-h {
syntax: '<number>';
inherits: true;
initial-value: 280;
}
.gbb-14 {
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: oklch(0.14 0.015 280);
animation: gbb14-drift 6s linear infinite;
}
.gbb-14 .gbb-14__plan {
display: grid;
justify-items: center;
gap: 14px;
}
.gbb-14 .gbb-14__badge {
display: inline-flex;
align-items: center;
gap: 7px;
font: 700 11px/1 'Segoe UI',system-ui,sans-serif;
letter-spacing: .09em;
text-transform: uppercase;
color: rgba(255,255,255,.85);
border: 1px solid oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .4);
border-radius: 999px;
padding: 6px 13px;
}
.gbb-14 .gbb-14__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: oklch(0.75 0.19 calc(var(--gbb-h) * 1deg));
box-shadow: 0 0 8px oklch(0.75 0.19 calc(var(--gbb-h) * 1deg));
}
.gbb-14 .gbb-14__btn {
cursor: pointer;
border: 2px solid transparent;
border-radius: 14px;
padding: 16px 32px;
font: 700 15px/1 'Segoe UI',system-ui,sans-serif;
color: #fff;
background: linear-gradient(oklch(0.18 0.02 280),oklch(0.18 0.02 280)) padding-box,linear-gradient(120deg,oklch(0.7 0.19 calc(var(--gbb-h) * 1deg)),oklch(0.7 0.19 calc((var(--gbb-h) + 60) * 1deg))) border-box;
box-shadow: 0 0 22px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .3);
transition: box-shadow .3s ease,transform .25s ease;
}
.gbb-14 .gbb-14__btn:hover,
.gbb-14 .gbb-14__btn:focus-visible {
transform: translateY(-2px);
box-shadow: 0 0 38px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .5),0 0 0 4px oklch(0.7 0.19 calc(var(--gbb-h) * 1deg) / .18);
}
.gbb-14:has(.gbb-14__btn:hover),
.gbb-14:has(.gbb-14__btn:focus-visible) {
animation-duration: 2.5s;
}
.gbb-14 .gbb-14__btn:focus-visible {
outline: 2px solid #fff;
outline-offset: 4px;
}
.gbb-14 .gbb-14__btn:active {
transform: translateY(0) scale(.98);
}
@keyframes gbb14-drift {
from {
--gbb-h: 0;
}
to {
--gbb-h: 360;
}
}
@media (prefers-reduced-motion: reduce) {
.gbb-14 {
animation: none;
}
}
```
## JavaScript
```js
/* No JavaScript — one registered number (--gbb-h) animates 0→360; every oklch() color on the button derives its hue from it via calc(). */
```How this works
Instead of animating a giant multi-stop gradient background-position (the old way — bandy and memory-hungry), this registers @property --gbb-h { syntax:'<number>' } and animates just that number 0→360. Every color on the button derives from it: border gradient stops at oklch(0.7 0.19 calc(var(--gbb-h) * 1deg)) and a second stop offset +60°, the halo shadow, even the badge dot. One animation, total palette coherence.
The oklch choice is the modern half of the lesson: in HSL, rotating hue at fixed s/l swings wildly through perceived brightness (yellow blinds, blue goes dark). OKLCH is perceptually uniform — pin L=0.7, C=0.19 and every hue lands at the same visual weight, so the border never has a 'weak frame'. The border itself is the padding-box/border-box gradient pair (same trick as demo 07), and hover simply widens the halo and speeds the drift from 6s to 2.5s — urgency through tempo, not size.
Make it yours
- Constrain the drift to a brand arc by animating 250→330 instead of 0→360 (alternate direction) for an on-palette shimmer.
- The +60° second stop sets gradient 'spread' — +180° gives a two-tone opposing border.
- Tempo: 6s rest / 2.5s hover shown; enterprise pricing pages read best at 10s+.
- Reuse --gbb-h on the parent card's border for a matched set highlighting the whole tier.
Gotchas — read before shipping
- syntax:'<number>' + calc(var() * 1deg) — animating a raw '<angle>' also works, but numbers let you reuse the value in non-angle contexts.
- Without @property registration, the hue snaps once per loop instead of drifting — the same failure mode as demo 01.
- Keep lightness ≥0.65 for dark themes; below that some hues fail 3:1 non-text contrast against near-black.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Floor set by :has(), oklch(), @property as this demo is written. The core technique itself goes back further — Chrome 85+.
@property + oklch() gate. Fallback: a static two-color gradient border — set the custom property's initial-value and skip the animation.