# Weight Wave (Moonarc)

A wave of boldness rolls through the letters of a line, each character animating its font weight on its own delay. Pure CSS. Continuous with a variable font; steps between available weights otherwise.

- Import: `import WeightWave from '@moonarc/core/WeightWave'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/weight-wave.json`
- Tier A · category text · trigger always
- Readout: `<WeightWave min={300} max={800}>`
- Browser support: widely (Chrome 62 · Firefox 62 · Safari 11)
- Measured cost: 0 B JS (CSS 4.5 kB raw)
- Page: https://moonarc.dev/components/weight-wave/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `text` | `string` | none | The text. |
| `as` | `HTMLTag` | `'span'` | Element to render. |
| `min` | `number` | `300` | Lightest weight. |
| `max` | `number` | `800` | Heaviest weight. |
| `duration` | `number` | `2.4` | Seconds per full wave at one character. |
| `step` | `number` | `80` | Delay between neighbouring characters, ms. Smaller is a faster-travelling wave. |

## Usage

```astro
<WeightWave as="h1" text="Variable" />
<WeightWave text="slow" duration={4} step={140} />
```

## Reduced motion

Every character sits at the inherited weight.

## With ClientRouter

CSS-only; nothing to rebind.

## Craft

- font-weight animates the wght axis directly; no font-variation-settings string to parse per frame.
- ease-in-out so the wave swells and recedes; a linear wave looks mechanical.
- Glyph widths change with weight, which means layout every frame: keep it to one line, and never in running text.
- A visually hidden span (.ma-sr) carries the text; the characters are aria-hidden.
- A character is a grapheme (Intl.Segmenter), so an emoji with a skin tone or a letter with a combining accent counts as one. The inline-block characters would follow the page's direction, so the element takes dir="auto" and Latin text on a right-to-left page still runs left to right.

## Replaces

- VariableProximity (React Bits)
- font-weight hover effects

## Source

```astro
---
/**
 * WeightWave — a wave of weight rolls through the letters, zero JS.
 *
 * Each character animates font-weight on its own delay. Needs a variable
 * font with a weight axis to be continuous; static families step between
 * the weights they have. Widths change with weight, so use it on a line
 * you want to breathe, not in a paragraph.
 */
import type { HTMLAttributes, HTMLTag } from 'astro/types';

interface Props extends HTMLAttributes<'span'> {
  /** The text. */
  text: string;
  /** Element to render. */
  as?: HTMLTag;
  /** Lightest weight. */
  min?: number;
  /** Heaviest weight. */
  max?: number;
  /** Seconds per full wave at one character. */
  duration?: number;
  /** Delay between neighbouring characters, ms. */
  step?: number;
}

const { text, as: Tag = 'span', min = 300, max = 800, duration = 2.4, step = 80, class: className, style, ...rest } = Astro.props;
// graphemes, not code points: an emoji with a skin tone or a letter with a combining accent swells as one character
const chars = Array.from(new Intl.Segmenter().segment(text), (g) => g.segment);
// the caller's style joins the component's properties instead of replacing them
const vars = [`--ma-ww-min:${min}`, `--ma-ww-max:${max}`, `--ma-ww-dur:${duration}s`, `--ma-ww-step:${step}ms`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
// dir="auto": the characters are inline-blocks, which bidi orders by the page's direction, so Latin text on a
// right-to-left page ran back to front. The element takes its direction from its own text instead.
---

<Tag class:list={['ma-weight', className]} dir="auto" style={vars} {...rest}>
  {chars.map((c, i) => (c === ' ' ? ' ' : <span class="ma-weight__char" aria-hidden="true" style={`--ma-i:${i}`}>{c}</span>))}
  <span class="ma-sr">{text}</span>
</Tag>

<style is:global>
  @layer components {
    :where(.ma-weight) {
      font-variation-settings: normal;
      white-space: pre-wrap;
    }
    :where(.ma-weight__char) {
      display: inline-block;
      font-weight: var(--ma-ww-min, 300);
      animation: ma-weight var(--ma-ww-dur, 2.4s) var(--ma-ease-in-out) infinite;
      animation-delay: calc(var(--ma-i, 0) * var(--ma-ww-step, 80ms));
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-weight__char) {
        animation: none;
        font-weight: inherit;
      }
    }
  }

  @keyframes ma-weight {
    0%,
    100% {
      font-weight: var(--ma-ww-min, 300);
    }
    50% {
      font-weight: var(--ma-ww-max, 800);
    }
  }
</style>

```
