Components / UI
Shimmer Button
A highlight sweeps across the body of a button on a loop; hover lifts it on the preset's hover travel and adds a ring, and focus draws the same. One pseudo-element gradient whose position animates: no layout, though every frame repaints the button's own box. Renders a <button> or an <a>. Zero JavaScript.
Live demo
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
Adjust · props are custom properties, set inline, so you edit the running instance
Measured
JavaScript of its own
0 B
CSS 5.6 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
every browser.
Install
One command adds the integration, the base tokens and every component. Then import what you use.
npx astro add moonarcpnpm astro add moonarcbunx astro add moonarc---
import ShimmerButton from '@moonarc/core/ShimmerButton';
---
<ShimmerButton>Get started</ShimmerButton>
<ShimmerButton as="a" href="/pricing/" color="oklch(90% 0.12 80)" background="oklch(45% 0.15 55)" text="#fff">See pricing →</ShimmerButton>Copy it into your project instead (shadcn registry)
Owns the file, no dependency. The registry item also installs the base tokens.
npx shadcn@latest add https://moonarc.dev/r/shimmer-button.jsonThe CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.
Source
The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.
---
/**
* ShimmerButton — a highlight sweeps across the body of a button on a loop,
* zero JS. The sheen is one pseudo-element gradient whose background-position
* animates: no layout, though every frame repaints the button's own box; the
* label sits above it. Hover lifts the button on the preset's hover travel and adds a
* ring; :focus-visible draws the same. Renders a <button> or an <a>.
*/
import type { HTMLAttributes } from 'astro/types';
interface Props extends HTMLAttributes<'button'> {
/** Element to render. */
as?: 'button' | 'a';
/** For `as="a"`. */
href?: string;
/** Sheen colour. */
color?: string;
/** Button background. */
background?: string;
/** Label colour. */
text?: string;
/** Seconds per sweep, pause included. */
duration?: number;
/** Corner radius, any CSS length. */
radius?: string;
}
const { as: Tag = 'button', href, color = 'var(--ma-shine)', background = 'var(--ma-ink)', text = 'var(--ma-panel)', duration = 3, radius = '999px', class: className, style, ...rest } = Astro.props;
const vars = [`--ma-shimmer-c:${color}`, `--ma-shimmer-bg:${background}`, `--ma-shimmer-fg:${text}`, `--ma-shimmer-dur:${duration}s`, `--ma-shimmer-r:${radius}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---
<Tag class:list={['ma-shimmer', className]} href={Tag === 'a' ? href : undefined} type={Tag === 'button' ? 'button' : undefined} style={vars} {...rest}>
<slot />
</Tag>
<style is:global>
@layer components {
:where(.ma-shimmer) {
position: relative;
isolation: isolate;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5em;
padding: 0.7em 1.4em;
border: 1px solid color-mix(in srgb, var(--ma-shimmer-fg, var(--ma-panel)) 18%, transparent);
border-radius: var(--ma-shimmer-r, 999px);
background: var(--ma-shimmer-bg, var(--ma-ink));
color: var(--ma-shimmer-fg, var(--ma-panel));
font: inherit;
font-weight: 500;
line-height: 1.2;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
overflow: clip;
transition:
translate var(--ma-duration-fast) var(--ma-ease-out),
box-shadow var(--ma-duration-fast) var(--ma-ease-out);
}
:where(.ma-shimmer)::before {
content: '';
position: absolute;
inset: -1px;
z-index: -1;
pointer-events: none;
background: linear-gradient(110deg, transparent 36%, var(--ma-shimmer-c, var(--ma-shine)) 50%, transparent 64%);
background-size: 250% 100%;
background-position: 120% 0;
animation: ma-shimmer var(--ma-shimmer-dur, 3s) var(--ma-ease-in-out) infinite;
}
@media (hover: hover) {
:where(.ma-shimmer:hover) {
translate: 0 calc(-0.5 * var(--ma-travel-hover));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--ma-shimmer-c, var(--ma-shine)) 55%, transparent);
}
}
:where(.ma-shimmer:focus-visible) {
/* --ma-shimmer-ring: the label colour is the ring by default; a section whose button inverts on its ground sets
the ground's own text colour here, since a ring in the label colour is the ground's colour (HeroWaitlist, 7G) */
outline: 2px solid var(--ma-shimmer-ring, currentColor);
outline-offset: 3px;
box-shadow: 0 0 0 1px color-mix(in srgb, var(--ma-shimmer-c, var(--ma-shine)) 55%, transparent);
}
:where(.ma-shimmer:active) {
translate: 0 0;
scale: 0.98;
}
@media (prefers-reduced-motion: reduce) {
:where(.ma-shimmer)::before {
animation: none;
display: none;
}
:where(.ma-shimmer:hover) {
translate: 0 0;
}
}
}
@keyframes ma-shimmer {
0% {
background-position: 120% 0;
}
55%,
100% {
background-position: -30% 0;
}
}
</style>Props
| Prop | Type | Default | Description |
|---|---|---|---|
as | 'button' | 'a' | 'button' | Element to render. |
href | string | none | For as="a". |
color | string | var(--ma-shine) | Sheen colour; the token is theme-aware. |
background | string | var(--ma-ink) | Button background. |
text | string | var(--ma-panel) | Label colour. |
duration | number | 3 | Seconds per sweep, pause included. |
radius | string | '999px' | Corner radius. |
--ma-shimmer-ring | CSS colour (custom property) | currentColor | The focus ring's colour, the label colour by default. Set it in a rule or style on the button or any ancestor when the button inverts on its ground. The label colour is then the ground's colour and the ring vanishes; the ground's own text colour is the ring that reads. |
Reduced motion
The sheen is removed; a plain button that still lifts nothing on hover.
With ClientRouter
CSS-only; nothing to rebind.
Why it is built this way
- The sheen runs 55 % of the period and rests the other 45 %: a loop that never pauses reads as a spinner.
- The gradient is a pseudo-element behind the label (z-index −1 inside an isolated stacking context), so the text is never tinted.
- Hover lifts by half the preset's hover travel: a button is heavier than a link.
- Ring on hover and focus-visible come from the same declaration, so keyboard users see exactly what pointer users see.
Replaces: ShimmerButton (Magic UI) · ShinyButton (Magic UI) · HoverBorderGradient (Aceternity). See the migration table.
Related
Border Beam
BackgroundsA beam of light travels around the edge of a card.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown