More Buttons Snippets
Shimmer Button — Free HTML CSS JS Animated Border Snippet
Shimmer Button · Buttons · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Shimmer Button — Rotating Spark Border and Sheen Sweep

The shimmer button is the premium call-to-action seen across modern AI and developer products: a dark pill wrapped in a thin animated light that travels around its border, with a glossy sheen that sweeps across the label on hover. This snippet builds it in plain HTML, CSS, and a touch of vanilla JavaScript, using a conic-gradient technique that stays cheap for the GPU.
The rotating spark border
The animated edge is created with two layers. The outer button holds a .sh-spark element whose ::before is an oversized square (160% of the button, kept square with aspect-ratio: 1) filled with a conic-gradient that is transparent for most of its sweep and bright purple-to-cyan for the final slice. Rotating that square 360° with the shSpin keyframe drags the bright slice around the perimeter. The inner .sh-body sits with a 1.5px margin on top of the spark, so only a thin rim of the rotating gradient peeks out as a glowing animated border — the body itself covers the rest.
Why conic-gradient over an SVG stroke
A rotating conic gradient masked by an inner panel is far cheaper than animating an SVG stroke or a box-shadow: it's a single transform on one element, which the compositor handles on the GPU without repainting. That's why the border can spin continuously without hurting scroll performance, even on a page full of other animations.
The sheen sweep
On hover, a .sh-shine overlay — a diagonal linear-gradient with a bright band in the middle — animates from translateX(-120%) to 120%, sweeping a glossy highlight across the label once. Because it starts and ends fully off the button, you only see the band crossing the face. The arrow icon also nudges right on hover, reinforcing the forward call to action.
A position-aware click ripple
The JavaScript adds tactile feedback. On click it reads the cursor position relative to the button, drops a tiny span there, and animates it with the Web Animations API (element.animate) scaling from 0 to 26× while fading out — a Material-style ripple that emanates from exactly where you clicked. The ripple element removes itself in the onfinish callback, so no nodes accumulate. Using animate() instead of CSS keyframes keeps the ripple self-contained and avoids managing extra classes.
Layering and isolation
The button uses overflow: hidden to clip the spark and ripple to its rounded shape, and isolation: isolate to keep the stacking context self-contained. The z-index order — spark at 0, body at 1, ripple at 2 — guarantees the ripple draws over the body while the spark stays behind it. The :active scale gives a subtle physical press.
Customizing it
Recolor the conic stops to change the spark, slow or speed the shSpin duration, widen the body margin for a thicker border, or adjust the sheen angle and speed. Make it a link by swapping <button> for <a> — the effects don't depend on the element. Pair it with a neo-brutalist card or place it inside an animated gradient CTA for a cohesive, high-end look.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA dark button renders with a thin light traveling around its border.
- 2Hover the buttonA glossy sheen sweeps across the label and the arrow slides right.
- 3Click itA ripple expands from exactly where you clicked.
- 4Recolor the sparkEdit the conic-gradient stops to match your brand.
- 5Adjust the speedChange the shSpin duration for a faster or calmer border.
- 6Use as a linkSwap button for an anchor; the effects still work.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
An outer spark layer has a ::before that is an oversized square filled with a conic-gradient — transparent for most of its sweep and bright for one slice. Rotating that square 360 degrees drags the bright slice around the perimeter. An inner body panel with a 1.5px margin covers everything except a thin rim, so only that glowing edge shows.
Rotating one conic-gradient element is a single GPU transform with no repaint, whereas animating an SVG stroke or a box-shadow forces repaints each frame. That's why the border can spin continuously without hurting scroll or competing animations, even on lower-end devices.
The click handler reads the pointer position relative to the button via getBoundingClientRect, places a small span at that point, and animates it with the Web Animations API from scale 0 to 26 while fading out. The element removes itself in the animation's onfinish callback, so ripples never accumulate in the DOM.
Yes. None of the effects depend on the button element — swap <button> for an <a href>. Keep the same class structure so the spark, body, sheen, and ripple markup line up. If you do, ensure the link has a visible focus style for keyboard users since the demo relies on the native button focus ring.
The markup and CSS port directly as a reusable Button component. Implement the ripple in the click handler using element.animate on a ref, or extract it to a small hook/composable. In Tailwind, define the conic spin keyframe in the config, build the border with an absolute spark layer plus an inset body, and animate the sheen with a translate-x utility on group-hover.
Shimmer Button — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shimmer Button</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#070712;display:flex;justify-content:center;align-items:center;min-height:100vh}
.sh-stage{padding:30px}
/* Outer button = the rotating conic spark ring */
.sh-btn{position:relative;border:none;background:none;padding:0;border-radius:13px;cursor:pointer;overflow:hidden;isolation:isolate}
.sh-spark{position:absolute;inset:0;z-index:0;border-radius:inherit}
.sh-spark::before{content:'';position:absolute;top:50%;left:50%;width:160%;aspect-ratio:1;transform:translate(-50%,-50%);background:conic-gradient(from 0deg,transparent 0 75%,#a78bfa 90%,#22d3ee 100%);animation:shSpin 2.6s linear infinite}
@keyframes shSpin{to{transform:translate(-50%,-50%) rotate(360deg)}}
/* Inner body sits 1.5px inside so the spark shows as a thin animated edge */
.sh-body{position:relative;z-index:1;display:inline-flex;align-items:center;gap:9px;margin:1.5px;padding:13px 24px;border-radius:11px;background:#141427;color:#fff;font-family:inherit;font-size:14.5px;font-weight:700;letter-spacing:.01em;overflow:hidden;transition:background .2s}
.sh-btn:hover .sh-body{background:#1b1b34}
.sh-body svg{width:17px;height:17px;transition:transform .2s}
.sh-btn:hover .sh-body svg{transform:translateX(3px)}
/* Sheen sweep across the label on hover */
.sh-shine{position:absolute;inset:0;background:linear-gradient(105deg,transparent 35%,rgba(255,255,255,.35) 50%,transparent 65%);transform:translateX(-120%);pointer-events:none}
.sh-btn:hover .sh-shine{animation:shSweep .85s ease}
@keyframes shSweep{to{transform:translateX(120%)}}
.sh-btn:active{transform:scale(.97)}
</style>
</head>
<body>
<div class="sh-stage">
<button type="button" class="sh-btn">
<span class="sh-spark" aria-hidden="true"></span>
<span class="sh-body">
<span class="sh-shine" aria-hidden="true"></span>
Get started
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"></path></svg>
</span>
</button>
</div>
<script>
// The shimmer and spark are pure CSS. JS only adds an accessible press ripple
// and a tiny click confirmation so the button feels responsive.
var btn = document.querySelector('.sh-btn');
var body = btn.querySelector('.sh-body');
btn.addEventListener('click', function (e) {
// Position-aware ripple from the click point.
var rect = btn.getBoundingClientRect();
var r = document.createElement('span');
r.style.cssText = 'position:absolute;z-index:2;border-radius:50%;pointer-events:none;' +
'background:rgba(255,255,255,.4);width:8px;height:8px;left:' + (e.clientX - rect.left) +
'px;top:' + (e.clientY - rect.top) + 'px;transform:translate(-50%,-50%);';
body.appendChild(r);
r.animate(
[{ transform: 'translate(-50%,-50%) scale(0)', opacity: .6 },
{ transform: 'translate(-50%,-50%) scale(26)', opacity: 0 }],
{ duration: 600, easing: 'ease-out' }
).onfinish = function () { r.remove(); };
});
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shimmer Button</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
@keyframes shSpin{to{transform:translate(-50%,-50%) rotate(360deg)}}
@keyframes shSweep{to{transform:translateX(120%)}}
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#070712;display:flex;justify-content:center;align-items:center;min-height:100vh
}
.sh-spark::before {
content:'';position:absolute;top:50%;left:50%;width:160%;aspect-ratio:1;transform:translate(-50%,-50%);background:conic-gradient(from 0deg,transparent 0 75%,#a78bfa 90%,#22d3ee 100%);animation:shSpin 2.6s linear infinite
}
.sh-btn:hover .sh-body {
background:#1b1b34
}
.sh-body svg {
width:17px;height:17px;transition:transform .2s
}
.sh-btn:hover .sh-body svg {
transform:translateX(3px)
}
.sh-btn:hover .sh-shine {
animation:shSweep .85s ease
}
</style>
</head>
<body>
<div class="p-[30px]">
<button type="button" class="sh-btn relative border-0 bg-transparent p-0 rounded-[13px] cursor-pointer overflow-hidden [isolation:isolate] active:[transform:scale(.97)]">
<span class="sh-spark absolute inset-0 z-0 rounded-[inherit]" aria-hidden="true"></span>
<span class="sh-body relative z-[1] inline-flex items-center gap-[9px] m-0.5 py-[13px] px-6 rounded-[11px] bg-[#141427] text-[#fff] font-[inherit] text-[14.5px] font-bold tracking-[.01em] overflow-hidden [transition:background_.2s]">
<span class="sh-shine absolute inset-0 [background:linear-gradient(105deg,transparent_35%,rgba(255,255,255,.35)_50%,transparent_65%)] [transform:translateX(-120%)] pointer-events-none" aria-hidden="true"></span>
Get started
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"></path></svg>
</span>
</button>
</div>
<script>
// The shimmer and spark are pure CSS. JS only adds an accessible press ripple
// and a tiny click confirmation so the button feels responsive.
var btn = document.querySelector('.sh-btn');
var body = btn.querySelector('.sh-body');
btn.addEventListener('click', function (e) {
// Position-aware ripple from the click point.
var rect = btn.getBoundingClientRect();
var r = document.createElement('span');
r.style.cssText = 'position:absolute;z-index:2;border-radius:50%;pointer-events:none;' +
'background:rgba(255,255,255,.4);width:8px;height:8px;left:' + (e.clientX - rect.left) +
'px;top:' + (e.clientY - rect.top) + 'px;transform:translate(-50%,-50%);';
body.appendChild(r);
r.animate(
[{ transform: 'translate(-50%,-50%) scale(0)', opacity: .6 },
{ transform: 'translate(-50%,-50%) scale(26)', opacity: 0 }],
{ duration: 600, easing: 'ease-out' }
).onfinish = function () { r.remove(); };
});
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to ShimmerButton.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#070712;display:flex;justify-content:center;align-items:center;min-height:100vh}
.sh-stage{padding:30px}
/* Outer button = the rotating conic spark ring */
.sh-btn{position:relative;border:none;background:none;padding:0;border-radius:13px;cursor:pointer;overflow:hidden;isolation:isolate}
.sh-spark{position:absolute;inset:0;z-index:0;border-radius:inherit}
.sh-spark::before{content:'';position:absolute;top:50%;left:50%;width:160%;aspect-ratio:1;transform:translate(-50%,-50%);background:conic-gradient(from 0deg,transparent 0 75%,#a78bfa 90%,#22d3ee 100%);animation:shSpin 2.6s linear infinite}
@keyframes shSpin{to{transform:translate(-50%,-50%) rotate(360deg)}}
/* Inner body sits 1.5px inside so the spark shows as a thin animated edge */
.sh-body{position:relative;z-index:1;display:inline-flex;align-items:center;gap:9px;margin:1.5px;padding:13px 24px;border-radius:11px;background:#141427;color:#fff;font-family:inherit;font-size:14.5px;font-weight:700;letter-spacing:.01em;overflow:hidden;transition:background .2s}
.sh-btn:hover .sh-body{background:#1b1b34}
.sh-body svg{width:17px;height:17px;transition:transform .2s}
.sh-btn:hover .sh-body svg{transform:translateX(3px)}
/* Sheen sweep across the label on hover */
.sh-shine{position:absolute;inset:0;background:linear-gradient(105deg,transparent 35%,rgba(255,255,255,.35) 50%,transparent 65%);transform:translateX(-120%);pointer-events:none}
.sh-btn:hover .sh-shine{animation:shSweep .85s ease}
@keyframes shSweep{to{transform:translateX(120%)}}
.sh-btn:active{transform:scale(.97)}
`;
export default function ShimmerButton() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
// The shimmer and spark are pure CSS. JS only adds an accessible press ripple
// and a tiny click confirmation so the button feels responsive.
var btn = document.querySelector('.sh-btn');
var body = btn.querySelector('.sh-body');
btn.addEventListener('click', function (e) {
// Position-aware ripple from the click point.
var rect = btn.getBoundingClientRect();
var r = document.createElement('span');
r.style.cssText = 'position:absolute;z-index:2;border-radius:50%;pointer-events:none;' +
'background:rgba(255,255,255,.4);width:8px;height:8px;left:' + (e.clientX - rect.left) +
'px;top:' + (e.clientY - rect.top) + 'px;transform:translate(-50%,-50%);';
body.appendChild(r);
r.animate(
[{ transform: 'translate(-50%,-50%) scale(0)', opacity: .6 },
{ transform: 'translate(-50%,-50%) scale(26)', opacity: 0 }],
{ duration: 600, easing: 'ease-out' }
).onfinish = function () { r.remove(); };
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="sh-stage">
<button type="button" className="sh-btn">
<span className="sh-spark" aria-hidden="true"></span>
<span className="sh-body">
<span className="sh-shine" aria-hidden="true"></span>
Get started
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"></path></svg>
</span>
</button>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function ShimmerButton() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
// The shimmer and spark are pure CSS. JS only adds an accessible press ripple
// and a tiny click confirmation so the button feels responsive.
var btn = document.querySelector('.sh-btn');
var body = btn.querySelector('.sh-body');
btn.addEventListener('click', function (e) {
// Position-aware ripple from the click point.
var rect = btn.getBoundingClientRect();
var r = document.createElement('span');
r.style.cssText = 'position:absolute;z-index:2;border-radius:50%;pointer-events:none;' +
'background:rgba(255,255,255,.4);width:8px;height:8px;left:' + (e.clientX - rect.left) +
'px;top:' + (e.clientY - rect.top) + 'px;transform:translate(-50%,-50%);';
body.appendChild(r);
r.animate(
[{ transform: 'translate(-50%,-50%) scale(0)', opacity: .6 },
{ transform: 'translate(-50%,-50%) scale(26)', opacity: 0 }],
{ duration: 600, easing: 'ease-out' }
).onfinish = function () { r.remove(); };
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
@keyframes shSpin{to{transform:translate(-50%,-50%) rotate(360deg)}}
@keyframes shSweep{to{transform:translateX(120%)}}
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#070712;display:flex;justify-content:center;align-items:center;min-height:100vh
}
.sh-spark::before {
content:'';position:absolute;top:50%;left:50%;width:160%;aspect-ratio:1;transform:translate(-50%,-50%);background:conic-gradient(from 0deg,transparent 0 75%,#a78bfa 90%,#22d3ee 100%);animation:shSpin 2.6s linear infinite
}
.sh-btn:hover .sh-body {
background:#1b1b34
}
.sh-body svg {
width:17px;height:17px;transition:transform .2s
}
.sh-btn:hover .sh-body svg {
transform:translateX(3px)
}
.sh-btn:hover .sh-shine {
animation:shSweep .85s ease
}
`}</style>
<div className="p-[30px]">
<button type="button" className="sh-btn relative border-0 bg-transparent p-0 rounded-[13px] cursor-pointer overflow-hidden [isolation:isolate] active:[transform:scale(.97)]">
<span className="sh-spark absolute inset-0 z-0 rounded-[inherit]" aria-hidden="true"></span>
<span className="sh-body relative z-[1] inline-flex items-center gap-[9px] m-0.5 py-[13px] px-6 rounded-[11px] bg-[#141427] text-[#fff] font-[inherit] text-[14.5px] font-bold tracking-[.01em] overflow-hidden [transition:background_.2s]">
<span className="sh-shine absolute inset-0 [background:linear-gradient(105deg,transparent_35%,rgba(255,255,255,.35)_50%,transparent_65%)] [transform:translateX(-120%)] pointer-events-none" aria-hidden="true"></span>
Get started
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"></path></svg>
</span>
</button>
</div>
</>
);
}<template>
<div class="sh-stage">
<button type="button" class="sh-btn">
<span class="sh-spark" aria-hidden="true"></span>
<span class="sh-body">
<span class="sh-shine" aria-hidden="true"></span>
Get started
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"></path></svg>
</span>
</button>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
let btn;
let body;
onMounted(() => {
btn = document.querySelector('.sh-btn');
body = btn.querySelector('.sh-body');
btn.addEventListener('click', function (e) {
// Position-aware ripple from the click point.
var rect = btn.getBoundingClientRect();
var r = document.createElement('span');
r.style.cssText = 'position:absolute;z-index:2;border-radius:50%;pointer-events:none;' +
'background:rgba(255,255,255,.4);width:8px;height:8px;left:' + (e.clientX - rect.left) +
'px;top:' + (e.clientY - rect.top) + 'px;transform:translate(-50%,-50%);';
body.appendChild(r);
r.animate(
[{ transform: 'translate(-50%,-50%) scale(0)', opacity: .6 },
{ transform: 'translate(-50%,-50%) scale(26)', opacity: 0 }],
{ duration: 600, easing: 'ease-out' }
).onfinish = function () { r.remove(); };
});
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#070712;display:flex;justify-content:center;align-items:center;min-height:100vh}
.sh-stage{padding:30px}
/* Outer button = the rotating conic spark ring */
.sh-btn{position:relative;border:none;background:none;padding:0;border-radius:13px;cursor:pointer;overflow:hidden;isolation:isolate}
.sh-spark{position:absolute;inset:0;z-index:0;border-radius:inherit}
.sh-spark::before{content:'';position:absolute;top:50%;left:50%;width:160%;aspect-ratio:1;transform:translate(-50%,-50%);background:conic-gradient(from 0deg,transparent 0 75%,#a78bfa 90%,#22d3ee 100%);animation:shSpin 2.6s linear infinite}
@keyframes shSpin{to{transform:translate(-50%,-50%) rotate(360deg)}}
/* Inner body sits 1.5px inside so the spark shows as a thin animated edge */
.sh-body{position:relative;z-index:1;display:inline-flex;align-items:center;gap:9px;margin:1.5px;padding:13px 24px;border-radius:11px;background:#141427;color:#fff;font-family:inherit;font-size:14.5px;font-weight:700;letter-spacing:.01em;overflow:hidden;transition:background .2s}
.sh-btn:hover .sh-body{background:#1b1b34}
.sh-body svg{width:17px;height:17px;transition:transform .2s}
.sh-btn:hover .sh-body svg{transform:translateX(3px)}
/* Sheen sweep across the label on hover */
.sh-shine{position:absolute;inset:0;background:linear-gradient(105deg,transparent 35%,rgba(255,255,255,.35) 50%,transparent 65%);transform:translateX(-120%);pointer-events:none}
.sh-btn:hover .sh-shine{animation:shSweep .85s ease}
@keyframes shSweep{to{transform:translateX(120%)}}
.sh-btn:active{transform:scale(.97)}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-shimmer-button',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="sh-stage">
<button type="button" class="sh-btn">
<span class="sh-spark" aria-hidden="true"></span>
<span class="sh-body">
<span class="sh-shine" aria-hidden="true"></span>
Get started
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"></path></svg>
</span>
</button>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#070712;display:flex;justify-content:center;align-items:center;min-height:100vh}
.sh-stage{padding:30px}
/* Outer button = the rotating conic spark ring */
.sh-btn{position:relative;border:none;background:none;padding:0;border-radius:13px;cursor:pointer;overflow:hidden;isolation:isolate}
.sh-spark{position:absolute;inset:0;z-index:0;border-radius:inherit}
.sh-spark::before{content:'';position:absolute;top:50%;left:50%;width:160%;aspect-ratio:1;transform:translate(-50%,-50%);background:conic-gradient(from 0deg,transparent 0 75%,#a78bfa 90%,#22d3ee 100%);animation:shSpin 2.6s linear infinite}
@keyframes shSpin{to{transform:translate(-50%,-50%) rotate(360deg)}}
/* Inner body sits 1.5px inside so the spark shows as a thin animated edge */
.sh-body{position:relative;z-index:1;display:inline-flex;align-items:center;gap:9px;margin:1.5px;padding:13px 24px;border-radius:11px;background:#141427;color:#fff;font-family:inherit;font-size:14.5px;font-weight:700;letter-spacing:.01em;overflow:hidden;transition:background .2s}
.sh-btn:hover .sh-body{background:#1b1b34}
.sh-body svg{width:17px;height:17px;transition:transform .2s}
.sh-btn:hover .sh-body svg{transform:translateX(3px)}
/* Sheen sweep across the label on hover */
.sh-shine{position:absolute;inset:0;background:linear-gradient(105deg,transparent 35%,rgba(255,255,255,.35) 50%,transparent 65%);transform:translateX(-120%);pointer-events:none}
.sh-btn:hover .sh-shine{animation:shSweep .85s ease}
@keyframes shSweep{to{transform:translateX(120%)}}
.sh-btn:active{transform:scale(.97)}
`]
})
export class ShimmerButtonComponent implements AfterViewInit {
ngAfterViewInit(): void {
// The shimmer and spark are pure CSS. JS only adds an accessible press ripple
// and a tiny click confirmation so the button feels responsive.
var btn = document.querySelector('.sh-btn');
var body = btn.querySelector('.sh-body');
btn.addEventListener('click', function (e) {
// Position-aware ripple from the click point.
var rect = btn.getBoundingClientRect();
var r = document.createElement('span');
r.style.cssText = 'position:absolute;z-index:2;border-radius:50%;pointer-events:none;' +
'background:rgba(255,255,255,.4);width:8px;height:8px;left:' + (e.clientX - rect.left) +
'px;top:' + (e.clientY - rect.top) + 'px;transform:translate(-50%,-50%);';
body.appendChild(r);
r.animate(
[{ transform: 'translate(-50%,-50%) scale(0)', opacity: .6 },
{ transform: 'translate(-50%,-50%) scale(26)', opacity: 0 }],
{ duration: 600, easing: 'ease-out' }
).onfinish = function () { r.remove(); };
});
}
}