Highlight Marker
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.
Live demo
Every cost here is measured from a real build and printed beside the component.
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
Measured
JavaScript of its own
0 B
Uses Reveal and the shared runtime (1.9 kB raw, once per site). With those included: 3.0 kB raw.
CSS 7.4 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 HighlightMarker from '@moonarc/core/HighlightMarker';
---
<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} />Copy it into your project instead (shadcn registry)
Owns the file, no dependency. The registry item also installs Scroll Reveal, the shared runtime and the base tokens.
npx shadcn@latest add https://moonarc.dev/r/highlight-marker.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. If you paste it, also copy Reveal.astro and runtime.ts, and add the JS gate to your head.
---
/**
* 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>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. |
Reduced motion
The phrases are highlighted at once.
With ClientRouter
Inherits Reveal: rebinds after every navigation.
Why it is built this way
- 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). See the migration table.
Related
Split Text
TextSplits a string into words, characters or lines on the server and reveals them with a capped stagger when they scroll into view.
Underline Hover
TextA link underline that enters from the left on hover and leaves to the right, so the exit continues the entrance instead of rewinding it.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown