Pure CSS Animated Border Trace Button
A single comet of light sweeps the button's perimeter on hover — no SVG, no JavaScript. A rotating conic-gradient wedge is clipped to a 2px border ring with the mask-composite trick, so the beam hugs any border-radius perfectly. The go-to 'expensive detail' for portfolio and dev-tool sites.
Published
The code
<section class="gbb-03" aria-label="Animated border trace button demo"> <a class="gbb-03__btn" href="#">View Case Study<span class="gbb-03__arrow" aria-hidden="true">→</span></a> </section>
<section class="gbb-03" aria-label="Animated border trace button demo">
<a class="gbb-03__btn" href="#">View Case Study<span class="gbb-03__arrow" aria-hidden="true">→</span></a>
</section>.gbb-03,
.gbb-03 *,
.gbb-03 *::before,
.gbb-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@property --gbb-a3 {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.gbb-03 {
--beam: oklch(0.85 0.12 200);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.16 0.015 260),oklch(0.12 0.01 260));
}
.gbb-03 .gbb-03__btn {
position: relative;
isolation: isolate;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 16px 30px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,.14);
background: oklch(0.18 0.015 260);
font: 600 15px/1 'Segoe UI',system-ui,sans-serif;
color: #f2f5f8;
text-decoration: none;
transition: border-color .3s ease;
}
.gbb-03 .gbb-03__btn::before,
.gbb-03 .gbb-03__btn::after {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
padding: 2px;
background: conic-gradient(from var(--gbb-a3),transparent 0 82%,color-mix(in oklab,var(--beam) 45%,transparent) 88%,var(--beam) 93%,transparent 97%);
-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;
pointer-events: none;
transition: opacity .3s ease;
}
.gbb-03 .gbb-03__btn::after {
filter: blur(8px);
inset: -3px;
padding: 4px;
}
.gbb-03 .gbb-03__btn:hover,
.gbb-03 .gbb-03__btn:focus-visible {
border-color: rgba(255,255,255,.22);
}
.gbb-03 .gbb-03__btn:hover::before,
.gbb-03 .gbb-03__btn:hover::after,
.gbb-03 .gbb-03__btn:focus-visible::before,
.gbb-03 .gbb-03__btn:focus-visible::after {
opacity: 1;
animation: gbb03-lap 1.4s linear infinite;
}
.gbb-03 .gbb-03__btn:focus-visible {
outline: 2px solid var(--beam);
outline-offset: 4px;
}
.gbb-03 .gbb-03__arrow {
transition: translate .3s ease;
}
.gbb-03 .gbb-03__btn:hover .gbb-03__arrow {
translate: 4px 0;
}
@keyframes gbb03-lap {
to {
--gbb-a3: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.gbb-03 .gbb-03__btn:hover::before,
.gbb-03 .gbb-03__btn:hover::after,
.gbb-03 .gbb-03__btn:focus-visible::before,
.gbb-03 .gbb-03__btn:focus-visible::after {
animation: none;
opacity: .6;
}
}.gbb-03,
.gbb-03 *,
.gbb-03 *::before,
.gbb-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@property --gbb-a3 {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.gbb-03 {
--beam: oklch(0.85 0.12 200);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.16 0.015 260),oklch(0.12 0.01 260));
}
.gbb-03 .gbb-03__btn {
position: relative;
isolation: isolate;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 16px 30px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,.14);
background: oklch(0.18 0.015 260);
font: 600 15px/1 'Segoe UI',system-ui,sans-serif;
color: #f2f5f8;
text-decoration: none;
transition: border-color .3s ease;
}
.gbb-03 .gbb-03__btn::before,
.gbb-03 .gbb-03__btn::after {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
padding: 2px;
background: conic-gradient(from var(--gbb-a3),transparent 0 82%,color-mix(in oklab,var(--beam) 45%,transparent) 88%,var(--beam) 93%,transparent 97%);
-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;
pointer-events: none;
transition: opacity .3s ease;
}
.gbb-03 .gbb-03__btn::after {
filter: blur(8px);
inset: -3px;
padding: 4px;
}
.gbb-03 .gbb-03__btn:hover,
.gbb-03 .gbb-03__btn:focus-visible {
border-color: rgba(255,255,255,.22);
}
.gbb-03 .gbb-03__btn:hover::before,
.gbb-03 .gbb-03__btn:hover::after,
.gbb-03 .gbb-03__btn:focus-visible::before,
.gbb-03 .gbb-03__btn:focus-visible::after {
opacity: 1;
animation: gbb03-lap 1.4s linear infinite;
}
.gbb-03 .gbb-03__btn:focus-visible {
outline: 2px solid var(--beam);
outline-offset: 4px;
}
.gbb-03 .gbb-03__arrow {
transition: translate .3s ease;
}
.gbb-03 .gbb-03__btn:hover .gbb-03__arrow {
translate: 4px 0;
}
@keyframes gbb03-lap {
to {
--gbb-a3: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.gbb-03 .gbb-03__btn:hover::before,
.gbb-03 .gbb-03__btn:hover::after,
.gbb-03 .gbb-03__btn:focus-visible::before,
.gbb-03 .gbb-03__btn:focus-visible::after {
animation: none;
opacity: .6;
}
}/* No JavaScript — a conic-gradient comet wedge is clipped to a 2px ring with mask-composite:exclude and spun via an @property angle. */
/* No JavaScript — a conic-gradient comet wedge is clipped to a 2px ring with mask-composite:exclude and spun via an @property angle. */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: Pure CSS Animated Border Trace Button
Source: https://codefronts.com/components/css-glowing-border-buttons/pure-css-animated-border-trace-button/
A single comet of light sweeps the button's perimeter on hover — no SVG, no JavaScript. A rotating conic-gradient wedge is clipped to a 2px border ring with the mask-composite trick, so the beam hugs any border-radius perfectly. The go-to 'expensive detail' for portfolio and dev-tool sites.
## HTML
```html
<section class="gbb-03" aria-label="Animated border trace button demo">
<a class="gbb-03__btn" href="#">View Case Study<span class="gbb-03__arrow" aria-hidden="true">→</span></a>
</section>
```
## CSS
```css
.gbb-03,
.gbb-03 *,
.gbb-03 *::before,
.gbb-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@property --gbb-a3 {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.gbb-03 {
--beam: oklch(0.85 0.12 200);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.16 0.015 260),oklch(0.12 0.01 260));
}
.gbb-03 .gbb-03__btn {
position: relative;
isolation: isolate;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 16px 30px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,.14);
background: oklch(0.18 0.015 260);
font: 600 15px/1 'Segoe UI',system-ui,sans-serif;
color: #f2f5f8;
text-decoration: none;
transition: border-color .3s ease;
}
.gbb-03 .gbb-03__btn::before,
.gbb-03 .gbb-03__btn::after {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
padding: 2px;
background: conic-gradient(from var(--gbb-a3),transparent 0 82%,color-mix(in oklab,var(--beam) 45%,transparent) 88%,var(--beam) 93%,transparent 97%);
-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;
pointer-events: none;
transition: opacity .3s ease;
}
.gbb-03 .gbb-03__btn::after {
filter: blur(8px);
inset: -3px;
padding: 4px;
}
.gbb-03 .gbb-03__btn:hover,
.gbb-03 .gbb-03__btn:focus-visible {
border-color: rgba(255,255,255,.22);
}
.gbb-03 .gbb-03__btn:hover::before,
.gbb-03 .gbb-03__btn:hover::after,
.gbb-03 .gbb-03__btn:focus-visible::before,
.gbb-03 .gbb-03__btn:focus-visible::after {
opacity: 1;
animation: gbb03-lap 1.4s linear infinite;
}
.gbb-03 .gbb-03__btn:focus-visible {
outline: 2px solid var(--beam);
outline-offset: 4px;
}
.gbb-03 .gbb-03__arrow {
transition: translate .3s ease;
}
.gbb-03 .gbb-03__btn:hover .gbb-03__arrow {
translate: 4px 0;
}
@keyframes gbb03-lap {
to {
--gbb-a3: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.gbb-03 .gbb-03__btn:hover::before,
.gbb-03 .gbb-03__btn:hover::after,
.gbb-03 .gbb-03__btn:focus-visible::before,
.gbb-03 .gbb-03__btn:focus-visible::after {
animation: none;
opacity: .6;
}
}
```
## JavaScript
```js
/* No JavaScript — a conic-gradient comet wedge is clipped to a 2px ring with mask-composite:exclude and spun via an @property angle. */
```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: Pure CSS Animated Border Trace Button
Source: https://codefronts.com/components/css-glowing-border-buttons/pure-css-animated-border-trace-button/
A single comet of light sweeps the button's perimeter on hover — no SVG, no JavaScript. A rotating conic-gradient wedge is clipped to a 2px border ring with the mask-composite trick, so the beam hugs any border-radius perfectly. The go-to 'expensive detail' for portfolio and dev-tool sites.
## HTML
```html
<section class="gbb-03" aria-label="Animated border trace button demo">
<a class="gbb-03__btn" href="#">View Case Study<span class="gbb-03__arrow" aria-hidden="true">→</span></a>
</section>
```
## CSS
```css
.gbb-03,
.gbb-03 *,
.gbb-03 *::before,
.gbb-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@property --gbb-a3 {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.gbb-03 {
--beam: oklch(0.85 0.12 200);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.16 0.015 260),oklch(0.12 0.01 260));
}
.gbb-03 .gbb-03__btn {
position: relative;
isolation: isolate;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 16px 30px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,.14);
background: oklch(0.18 0.015 260);
font: 600 15px/1 'Segoe UI',system-ui,sans-serif;
color: #f2f5f8;
text-decoration: none;
transition: border-color .3s ease;
}
.gbb-03 .gbb-03__btn::before,
.gbb-03 .gbb-03__btn::after {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
padding: 2px;
background: conic-gradient(from var(--gbb-a3),transparent 0 82%,color-mix(in oklab,var(--beam) 45%,transparent) 88%,var(--beam) 93%,transparent 97%);
-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;
pointer-events: none;
transition: opacity .3s ease;
}
.gbb-03 .gbb-03__btn::after {
filter: blur(8px);
inset: -3px;
padding: 4px;
}
.gbb-03 .gbb-03__btn:hover,
.gbb-03 .gbb-03__btn:focus-visible {
border-color: rgba(255,255,255,.22);
}
.gbb-03 .gbb-03__btn:hover::before,
.gbb-03 .gbb-03__btn:hover::after,
.gbb-03 .gbb-03__btn:focus-visible::before,
.gbb-03 .gbb-03__btn:focus-visible::after {
opacity: 1;
animation: gbb03-lap 1.4s linear infinite;
}
.gbb-03 .gbb-03__btn:focus-visible {
outline: 2px solid var(--beam);
outline-offset: 4px;
}
.gbb-03 .gbb-03__arrow {
transition: translate .3s ease;
}
.gbb-03 .gbb-03__btn:hover .gbb-03__arrow {
translate: 4px 0;
}
@keyframes gbb03-lap {
to {
--gbb-a3: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.gbb-03 .gbb-03__btn:hover::before,
.gbb-03 .gbb-03__btn:hover::after,
.gbb-03 .gbb-03__btn:focus-visible::before,
.gbb-03 .gbb-03__btn:focus-visible::after {
animation: none;
opacity: .6;
}
}
```
## JavaScript
```js
/* No JavaScript — a conic-gradient comet wedge is clipped to a 2px ring with mask-composite:exclude and spun via an @property angle. */
```How this works
Two techniques stacked. First, the ring: an overlay ::before covers the button, gets padding:2px, and is masked with two stacked layers — mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0) — combined with mask-composite:exclude. The content-box layer punches out the middle, leaving exactly the 2px band between padding edge and outer edge. This is the modern replacement for nested border wrappers, and it follows border-radius for free.
Second, the comet: the ring's background is conic-gradient(from var(--gbb-a), transparent 0 82%, var(--beam) 92%, transparent 97%) — mostly transparent with one bright 15% wedge whose leading edge is sharp and trailing edge fades, i.e. a tail. Animating the registered --gbb-a spins the wedge around the ring. A second blurred pseudo-element re-uses the same gradient as the beam's outer glow. The animation only runs on :hover/:focus-visible, so the page at rest costs nothing.
Make it yours
- Trace speed: 1.2s per lap feels snappy; 2.5s feels like a scanner. Set it on the
animationshorthand. - Two comets: duplicate the wedge 180° apart inside the same conic-gradient stop list.
- Leave a faint static ring at rest by giving the ring layer a low-opacity base color behind the wedge.
- Beam color is one variable (
--beam) — match it to your accent, or use white for a 'light on metal' look.
Gotchas — read before shipping
- Write both
-webkit-maskandmask; Safari still needs the prefix and silently shows a filled rectangle without it. mask-composite:exclude(standard) pairs with-webkit-mask-composite:xor— the keywords differ between the two syntaxes.- The overlay must be
pointer-events:noneor hovering the border stutters the trace.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 120+ | 15.4+ | 128+ | 120+ |
Gated by mask-composite + @property together. No @property → the comet jumps instead of gliding; no mask → show a plain border fallback.