🚀

Update available

We just released a new resource or update, refresh the Vault to access the latest version.

Cancel

Refresh now

Harri

Profile Picture

Harri

Lemke

Highlight Marker Text Reveal

Documentation

Webflow

Code

Setup: External Scripts

HTML

Copy
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/SplitText.min.js"></script>

Step 1: Add HTML

HTML

Copy
<h1 data-highlight-marker-reveal data-marker-direction="right" data-marker-theme="pink" class="highlight-title">Here's a text reveal that looks like a highlight marker</h1>

Step 2: Add CSS

CSS

Copy
.highlight-title {
  text-align: center;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  margin-top: 0;
  margin-bottom: 0;
  font-family: Haffer, Arial, sans-serif;
  font-size: 6vw;
  font-weight: 900;
  line-height: 0.9;
}

[data-highlight-marker-reveal]{
  visibility: hidden;
}

[data-highlight-marker-reveal] .highlight-marker-line{
  width: auto;
  display: inline-block !important;
  margin: -0.055em 0px;
}

.highlight-marker-bar{
  position: absolute;
  inset: -0.055em 0px;
  z-index: 1;
  pointer-events: none;
}

Step 2: Add Javascript

Step 3: Add Javascript

Javascript

Copy
function initHighlightMarkerTextReveal() {
  const defaults = {
    direction: "right",
    theme: "pink",
    scrollStart: "top 90%",
    staggerStart: "start",
    stagger: 100,
    barDuration: 0.6,
    barEase: "power3.inOut",
  };

  const colorMap = {
    pink: "#C700EF",
    white: "#FFFFFF",
  };

  const directionMap = {
    right: { prop: "scaleX", origin: "right center" },
    left: { prop: "scaleX", origin: "left center" },
    up: { prop: "scaleY", origin: "center top" },
    down: { prop: "scaleY", origin: "center bottom" },
  };

  function resolveColor(value) {
    if (colorMap[value]) return colorMap[value];
    if (value.startsWith("--")) {
      return getComputedStyle(document.body).getPropertyValue(value).trim() || value;
    }
    return value;
  }

  function createBar(color, origin) {
    const bar = document.createElement("div");
    bar.className = "highlight-marker-bar";
    Object.assign(bar.style, {
      backgroundColor: color,
      transformOrigin: origin,
    });
    return bar;
  }

  function cleanupElement(el) {
    if (!el._highlightMarkerReveal) return;
    el._highlightMarkerReveal.timeline?.kill();
    el._highlightMarkerReveal.scrollTrigger?.kill();
    el._highlightMarkerReveal.split?.revert();
    el.querySelectorAll(".highlight-marker-bar").forEach((bar) => bar.remove());
    delete el._highlightMarkerReveal;
  }

  let reduceMotion = false;

  gsap.matchMedia().add(
    { reduce: "(prefers-reduced-motion: reduce)" },
    (context) => {
      reduceMotion = context.conditions.reduce;
    }
  );

  // Reduced motion: no animation at all
  if (reduceMotion) {
    document.querySelectorAll("[data-highlight-marker-reveal]").forEach((el) => {
      gsap.set(el, { autoAlpha: 1 });
    });
    return;
  }

  // Cleanup previous instances
  document.querySelectorAll("[data-highlight-marker-reveal]").forEach(cleanupElement);

  const elements = document.querySelectorAll("[data-highlight-marker-reveal]");
  if (!elements.length) return;

  elements.forEach((el) => {
    const direction = el.getAttribute("data-marker-direction") || defaults.direction;
    const theme = el.getAttribute("data-marker-theme") || defaults.theme;
    const scrollStart = el.getAttribute("data-marker-scroll-start") || defaults.scrollStart;
    const staggerStart = el.getAttribute("data-marker-stagger-start") || defaults.staggerStart;
    const staggerOffset = (parseFloat(el.getAttribute("data-marker-stagger")) || defaults.stagger) / 1000;

    const color = resolveColor(theme);
    const dirConfig = directionMap[direction] || directionMap.right;

    el._highlightMarkerReveal = {};

    const split = SplitText.create(el, {
      type: "lines",
      linesClass: "highlight-marker-line",
      autoSplit: true,
      onSplit(self) {
        const instance = el._highlightMarkerReveal;

        // Teardown previous build
        instance.timeline?.kill();
        instance.scrollTrigger?.kill();
        el.querySelectorAll(".highlight-marker-bar").forEach((bar) => bar.remove());

        // Build bars and timeline
        const lines = self.lines;
        const tl = gsap.timeline({ paused: true });

        lines.forEach((line, i) => {
          gsap.set(line, { position: "relative", overflow: "hidden" });

          const bar = createBar(color, dirConfig.origin);
          line.appendChild(bar);

          const staggerIndex = staggerStart === "end" ? lines.length - 1 - i : i;

          tl.to(bar, {
            [dirConfig.prop]: 0,
            duration: defaults.barDuration,
            ease: defaults.barEase,
          }, staggerIndex * staggerOffset);
        });

        // Reveal parent — bars are covering the text
        gsap.set(el, { autoAlpha: 1 });

        // ScrollTrigger
        const st = ScrollTrigger.create({
          trigger: el,
          start: scrollStart,
          once: true,
          onEnter: () => tl.play(),
        });

        instance.timeline = tl;
        instance.scrollTrigger = st;
      },
    });

    el._highlightMarkerReveal.split = split;
  });
}

// Initialize Highlight Marker Text Reveal
document.addEventListener("DOMContentLoaded", () => {
  document.fonts.ready.then(() => {
    initHighlightMarkerTextReveal();
  });
});

Implementation

Targets

Add [data-highlight-marker-reveal] to any text element to activate the highlight marker reveal. Each line of text will be covered by a colored bar that scales away on scroll, revealing the content beneath. The script uses GSAP SplitText to split the text into lines, so no manual line wrapping is needed.

Direction

Control which way the bar moves away with [data-marker-direction] (default right). Accepted values are left, right, up, and down, the bar anchors to the named edge and scales toward it, revealing text from the opposite side.

Theme

The bar color is set through [data-marker-theme] (default pink), which accepts a named color key, a CSS custom property, or any raw color value. Named keys resolve through a color map defined in the JavaScript. You can add as many options in here as you want.

const colorMap = {  
  pink: "#C700EF", 
  white: "#FFFFFF",
};
Copy

To read a CSS variable from the body, pass the variable name including the dashes:

<h2 data-highlight-marker-reveal data-marker-theme="--brand-accent">Heading</h2>
Copy

Any value that isn't a named key or CSS variable is used as-is, so #ff6600, rgb(255, 102, 0), or any valid CSS color works directly.

Scroll Start

The scroll trigger fires by default when the element reaches 90% of the viewport. Override this per element with [data-marker-scroll-start], which accepts any valid ScrollTrigger start value.

Stagger

Lines animate in sequence with a default offset of 100ms between each line. Adjust this per element with [data-marker-stagger] in milliseconds. A value of 500 for example will make each bar stagger with 0.5s of delay.

Stagger Start

By default, lines reveal from top to bottom. You can flip the order, and make the sequence move bottom-to-top by adding [data-marker-stagger-start="end"] to the target.

Bar Height and Spacing

The visible height of the bars and the spacing between them is controlled entirely through CSS. Three properties work together: the line-height on the text element, and the inset and margin values on the line and bar.

[data-highlight-marker-reveal] .highlight-marker-line {
  margin: -0.055em 0px;
}

.highlight-marker-bar {
  inset: -0.055em 0px;
}
Copy

The negative margin on .highlight-marker-line pulls lines closer together, while the negative top/bottom inset on .highlight-marker-bar extends the bar beyond the line box. Together with the element's line-height, these three values determine whether bars overlap, touch seamlessly, or have visible gaps between them. Increase the negative values to create taller bars that bleed into adjacent lines, or reduce them to leave breathing room between each highlighted row.

Defaults

All attributes are optional. When omitted, values fall back to a defaults object defined at the top of the script. Adjust these to change the baseline behavior across all instances at once.

const defaults = {
  direction: "right",
  theme: "pink",
  scrollStart: "top 90%",
  staggerStart: "start",
  stagger: 100,
  barDuration: 0.6,
  barEase: "power3.inOut",
};
Copy

The barDuration and barEase are only configurable through this object and don't have corresponding attributes, they're meant to stay consistent across the page.

Live preview

Osmo Robot AI

Copy context for AI

Beta

Webflow

HTML/CSS/JS

Save video

Copy share link

Resource details

  • Published

    February 18, 2026

  • Category

    Text Animations

  • Popularity

    915 visitors

  • Need help?

    Join Slack

GSAP
SplitText
Scrolltrigger
Text Reveal
Direction
Reveal
Line
Loading

Original source

Ilja van EckIlja van Eck

Creator Credits

We always strive to credit creators as accurately as possible. While similar concepts might appear online, we aim to provide proper and respectful attribution.

s