Skip to content

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

See pricing →

Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.

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

Baseline · widely available

every browser.

Install

One command adds the integration, the base tokens and every component. Then import what you use.

npx astro add moonarc
pnpm astro add moonarc
bunx astro add moonarc
src/pages/index.astro
---
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.

terminal
npx shadcn@latest add https://moonarc.dev/r/shimmer-button.json

The 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.astro
---
/**
 * 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

PropTypeDefaultDescription
as'button' | 'a''button'Element to render.
hrefstringnoneFor as="a".
colorstringvar(--ma-shine)Sheen colour; the token is theme-aware.
backgroundstringvar(--ma-ink)Button background.
textstringvar(--ma-panel)Label colour.
durationnumber3Seconds per sweep, pause included.
radiusstring'999px'Corner radius.
--ma-shimmer-ringCSS colour (custom property)currentColorThe 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

Replaces: ShimmerButton (Magic UI) · ShinyButton (Magic UI) · HoverBorderGradient (Aceternity). See the migration table.

A button whose arrow slides out on hover as a new one slides in.

every browserhover

Border Beam

Backgrounds

A beam of light travels around the edge of a card.

newly · Chrome 120 · Firefox 128 · Safari 16.4always

A highlight sweeps across the text every few seconds.

every browseralways

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown