# Highlight Marker (Moonarc)

A highlighter sweeps across the phrases you marked, one after another, when the sentence scrolls into view. Each phrase is a real <mark> whose background grows from the left; wrapped lines are painted as separate strokes. Composes Reveal; no script of its own.

- Import: `import HighlightMarker from '@moonarc/core/HighlightMarker'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/highlight-marker.json`
- Tier A · category text · trigger scroll
- Readout: `<HighlightMarker text="…**two phrases**…">`
- Browser support: widely (every browser)
- Measured cost: 0 B JS of its own · uses Reveal + runtime (with dependencies 3.0 kB raw; CSS 7.4 kB raw)
- Page: https://moonarc.dev/components/highlight-marker/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `text` | `string` | none | The sentence. Wrap each phrase to highlight in double asterisks; they sweep in order. |
| `as` | `HTMLTag` | `'p'` | Element to render. |
| `color` | `string` | `currentColor at 22%` | Highlight colour. Keep it translucent so the text stays legible in both themes. |
| `duration` | `number` | `preset (ui: 450)` | Sweep duration per phrase in ms; the next phrase starts when the previous lands. |
| `delay` | `number` | `0` | Delay before the first sweep. |
| `once` | `boolean` | `true` | Play once and stay. |
| `threshold` | `number` | `0.2` | Fraction visible to trigger. |

## Usage

```astro
<HighlightMarker text="Every cost here is **measured from a real build** and printed **beside the component**." />
<HighlightMarker as="h2" text="**Zero bytes** of JavaScript." color="color-mix(in oklch, var(--ma-shine) 60%, transparent)" duration={600} />
```

## Reduced motion

The phrases are highlighted at once.

## With ClientRouter

Inherits Reveal: rebinds after every navigation.

## Craft

- background-size on a solid gradient, not a pseudo-element: the sweep follows the text's own line boxes, and box-decoration-break: clone gives every wrapped line its own stroke start.
- Sequential: phrase n starts when phrase n − 1 lands, because a highlighter is one hand.
- Ease-out: a marker moves fast and stops without accelerating into the last word.
- <mark> is the semantic element for a highlight, so the emphasis is in the accessibility tree and the HTML is readable without any CSS.

## Replaces

- Highlighter (Magic UI)
- Highlight (Aceternity hero highlight)

## Source

```astro
---
/**
 * HighlightMarker — a highlighter sweeps across phrases, one after another,
 * when the text scrolls into view. Phrases are marked **like this** in the
 * prop and rendered as <mark> elements whose background-size grows from 0
 * to 100% on a per-phrase delay; box-decoration-break keeps the sweep on
 * wrapped lines. Composes Reveal; zero script of its own. The server HTML
 * is the plain sentence with the marks already in place.
 */
import type { HTMLAttributes, HTMLTag } from 'astro/types';
import Reveal from './Reveal.astro';

interface Props extends HTMLAttributes<'p'> {
  /** The sentence; wrap phrases to highlight in double asterisks. */
  text: string;
  /** Element to render. */
  as?: HTMLTag;
  /** Highlight colour; a translucent colour keeps the text legible. */
  color?: string;
  /** Sweep duration per phrase in ms. Unset: the preset's --ma-duration. */
  duration?: number;
  /** Delay in ms before the first sweep. */
  delay?: number;
  /** Play once and stay. */
  once?: boolean;
  /** Fraction visible to trigger. */
  threshold?: number;
}

const { text, as = 'p', color = 'color-mix(in oklch, currentColor 22%, transparent)', duration, delay, once, threshold, class: className, style, ...rest } = Astro.props;
const parts = text.split(/\*\*([^*]+)\*\*/);
let mark = 0;
const vars = [`--ma-marker-color:${color}`, duration !== undefined && `--ma-marker-dur:${duration}ms`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---

<Reveal as={as} from="none" delay={delay} once={once} threshold={threshold} class={['ma-marker', className].filter(Boolean).join(' ')} style={vars} {...rest}>
  {parts.map((part, i) => (i % 2 ? <mark class="ma-marker__mark" style={`--ma-marker-i:${mark++}`}>{part}</mark> : part))}
</Reveal>

<style is:global>
  @layer components {
    :where(.ma-marker__mark) {
      color: inherit;
      background-color: transparent;
      background-image: linear-gradient(var(--ma-marker-color), var(--ma-marker-color));
      background-repeat: no-repeat;
      background-position: 0 0;
      background-size: 100% 100%;
      padding: 0.08em 0.18em;
      margin-inline: -0.18em;
      border-radius: 0.25em;
      -webkit-box-decoration-break: clone;
      box-decoration-break: clone;
      /* each phrase waits for the one before it; the delay is the one Reveal writes on this root, so the sweep keeps
         step with the sentence's fade */
      transition: background-size var(--ma-marker-dur, var(--ma-duration)) var(--ma-ease-out) calc(var(--ma-r-delay, 0ms) + var(--ma-marker-i, 0) * var(--ma-marker-dur, var(--ma-duration)));
    }
    :where([data-ma-js] .ma-marker:not(.ma-in) .ma-marker__mark) {
      background-size: 0% 100%;
      transition: none;
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-marker__mark) {
        transition: none;
      }
    }
  }
</style>

```
