# Shimmer Button (Moonarc)

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.

- Import: `import ShimmerButton from '@moonarc/core/ShimmerButton'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/shimmer-button.json`
- Tier A · category ui · trigger always, hover
- Readout: `<ShimmerButton duration={3}>`
- Browser support: widely (every browser)
- Measured cost: 0 B JS (CSS 5.6 kB raw)
- Page: https://moonarc.dev/components/shimmer-button/

## 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. |

## Usage

```astro
<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>
```

## Reduced motion

The sheen is removed; a plain button that still lifts nothing on hover.

## With ClientRouter

CSS-only; nothing to rebind.

## Craft

- 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)

## Source

```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>

```
