Scroll down to see the animations in action
This text animates as you scroll. Each character lights up with the accent color, then transitions to the final color. Scroll back up to reverse it.
This text plays the animation once when it enters the viewport. It won't reverse when you scroll back up. Perfect for hero text and headlines.
In an era defined by precision and speed, innovation reshapes the foundation of modern industry. Every component is built with intent, every system designed to perform at scale.
Every detail is calibrated for efficiency—turning raw energy into refined, sustainable power that drives industries forward.
Add these to your <head> or before closing </body> tag:
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/SplitText.min.js"></script>[data-terminal-reveal],
[data-terminal-reveal-once] {
opacity: 0;
}
[data-terminal-reveal].is-ready,
[data-terminal-reveal-once].is-ready {
opacity: 1;
}
.word {
display: inline-block;
white-space: nowrap;
}
.char {
display: inline-block;
}This function animates text while scrolling. Scroll position controls the animation progress.
/**
* Terminal Text Reveal - Scrub Mode
* Animates text based on scroll position (reversible)
*
* @param {string} selector - Attribute selector (e.g., "[data-terminal-reveal]")
*/
function initTerminalRevealScrub(selector = "[data-terminal-reveal]") {
gsap.registerPlugin(ScrollTrigger, SplitText);
document.querySelectorAll(selector).forEach((element) => {
// Get customization options from data attributes
const colorInitial = element.dataset.colorInitial || "#dddddd";
const colorAccent = element.dataset.colorAccent || "#abff02";
const colorFinal = element.dataset.colorFinal || "#000000";
// State tracking
let lastScrollProgress = 0;
const colorTransitionTimers = new Map();
const completedChars = new Set();
// Split text into words, then chars
const wordSplit = SplitText.create(element, {
type: "words",
wordsClass: "word",
});
const charSplit = SplitText.create(wordSplit.words, {
type: "chars",
charsClass: "char",
});
const allChars = charSplit.chars;
// Set initial color
gsap.set(allChars, { color: colorInitial });
// Mark as ready (triggers opacity: 1)
element.classList.add("is-ready");
// Schedule final color transition with delay
const scheduleFinalTransition = (char, index) => {
if (colorTransitionTimers.has(index)) {
clearTimeout(colorTransitionTimers.get(index));
}
const timer = setTimeout(() => {
if (!completedChars.has(index)) {
gsap.to(char, {
duration: 0.1,
ease: "none",
color: colorFinal,
onComplete: () => completedChars.add(index),
});
}
colorTransitionTimers.delete(index);
}, 100);
colorTransitionTimers.set(index, timer);
};
// Create ScrollTrigger with scrub
ScrollTrigger.create({
trigger: element,
start: "top 90%",
end: "top 10%",
scrub: 1,
onUpdate: (self) => {
const progress = self.progress;
const totalChars = allChars.length;
const isScrollingDown = progress >= lastScrollProgress;
const currentCharIndex = Math.floor(progress * totalChars);
allChars.forEach((char, index) => {
// Scrolling up - reset chars
if (!isScrollingDown && index >= currentCharIndex) {
if (colorTransitionTimers.has(index)) {
clearTimeout(colorTransitionTimers.get(index));
colorTransitionTimers.delete(index);
}
completedChars.delete(index);
gsap.set(char, { color: colorInitial });
return;
}
// Skip already completed chars
if (completedChars.has(index)) return;
// Animate current and past chars
if (index <= currentCharIndex) {
gsap.set(char, { color: colorAccent });
if (!colorTransitionTimers.has(index)) {
scheduleFinalTransition(char, index);
}
} else {
gsap.set(char, { color: colorInitial });
}
});
lastScrollProgress = progress;
},
});
});
}This function plays the animation once when the element scrolls into view. Non-reversible.
/**
* Terminal Text Reveal - Once Mode
* Plays animation once when element enters viewport (non-reversible)
*
* @param {string} selector - Attribute selector (e.g., "[data-terminal-reveal-once]")
*/
function initTerminalRevealOnce(selector = "[data-terminal-reveal-once]") {
gsap.registerPlugin(ScrollTrigger, SplitText);
document.querySelectorAll(selector).forEach((element) => {
// Get customization options from data attributes
const colorInitial = element.dataset.colorInitial || "#dddddd";
const colorAccent = element.dataset.colorAccent || "#abff02";
const colorFinal = element.dataset.colorFinal || "#000000";
const duration = parseFloat(element.dataset.duration) || 1;
const stagger = parseFloat(element.dataset.stagger) || 0.02;
// Split text into words, then chars
const wordSplit = SplitText.create(element, {
type: "words",
wordsClass: "word",
});
const charSplit = SplitText.create(wordSplit.words, {
type: "chars",
charsClass: "char",
});
const allChars = charSplit.chars;
// Set initial color
gsap.set(allChars, { color: colorInitial });
// Mark as ready (triggers opacity: 1)
element.classList.add("is-ready");
// Create the animation timeline
const tl = gsap.timeline({ paused: true });
// Animate each character: initial -> accent -> final
allChars.forEach((char, index) => {
const charTl = gsap.timeline();
// Flash to accent color
charTl.to(char, {
color: colorAccent,
duration: duration * 0.1,
ease: "power2.out",
});
// Transition to final color
charTl.to(char, {
color: colorFinal,
duration: duration * 0.3,
ease: "power2.inOut",
});
// Add to main timeline with stagger
tl.add(charTl, index * stagger);
});
// Trigger animation once on scroll into view
ScrollTrigger.create({
trigger: element,
start: "top 85%",
once: true,
onEnter: () => tl.play(),
});
});
}document.addEventListener("DOMContentLoaded", () => {
// Initialize scrub mode (animates while scrolling)
initTerminalRevealScrub("[data-terminal-reveal]");
// Initialize once mode (plays once on scroll into view)
initTerminalRevealOnce("[data-terminal-reveal-once]");
});<!-- Scrub Mode (animates while scrolling, reversible) -->
<p data-terminal-reveal>
Your text here will animate as you scroll.
</p>
<!-- Once Mode (plays once on scroll into view) -->
<h1 data-terminal-reveal-once>
Your headline plays animation once when visible.
</h1>
<!-- Custom colors -->
<p
data-terminal-reveal
data-color-initial="#444444"
data-color-accent="#ff6b6b"
data-color-final="#ffffff"
>
Custom colored animation.
</p>
<!-- Custom timing (once mode only) -->
<h2
data-terminal-reveal-once
data-color-accent="#00ffff"
data-duration="1.5"
data-stagger="0.03"
>
Slower animation with more stagger.
</h2>| Attribute | Default | Description |
|---|---|---|
data-terminal-reveal | - | Enables scrub mode (scroll-linked animation) |
data-terminal-reveal-once | - | Enables once mode (plays once on scroll into view) |
data-color-initial | #dddddd | Starting color of the text |
data-color-accent | #abff02 | Flash/highlight color during reveal |
data-color-final | #000000 | Final color after animation completes |
data-duration | 1 | Animation duration in seconds (once mode only) |
data-stagger | 0.02 | Delay between each character (once mode only) |
data-terminal-reveal or data-terminal-reveal-once as a custom attribute to any text element. Then add the CSS to your project's global styles and the JavaScript to a custom code embed before the closing </body> tag.Copy this entire script and paste it before your closing </body> tag:
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/SplitText.min.js"></script>
<script>
gsap.registerPlugin(ScrollTrigger, SplitText);
function initTerminalRevealScrub(selector = "[data-terminal-reveal]") {
document.querySelectorAll(selector).forEach((element) => {
const colorInitial = element.dataset.colorInitial || "#dddddd";
const colorAccent = element.dataset.colorAccent || "#abff02";
const colorFinal = element.dataset.colorFinal || "#000000";
let lastScrollProgress = 0;
const colorTransitionTimers = new Map();
const completedChars = new Set();
const wordSplit = SplitText.create(element, { type: "words", wordsClass: "word" });
const charSplit = SplitText.create(wordSplit.words, { type: "chars", charsClass: "char" });
const allChars = charSplit.chars;
gsap.set(allChars, { color: colorInitial });
element.classList.add("is-ready");
const scheduleFinalTransition = (char, index) => {
if (colorTransitionTimers.has(index)) clearTimeout(colorTransitionTimers.get(index));
const timer = setTimeout(() => {
if (!completedChars.has(index)) {
gsap.to(char, { duration: 0.1, ease: "none", color: colorFinal, onComplete: () => completedChars.add(index) });
}
colorTransitionTimers.delete(index);
}, 100);
colorTransitionTimers.set(index, timer);
};
ScrollTrigger.create({
trigger: element,
start: "top 90%",
end: "top 10%",
scrub: 1,
onUpdate: (self) => {
const progress = self.progress;
const totalChars = allChars.length;
const isScrollingDown = progress >= lastScrollProgress;
const currentCharIndex = Math.floor(progress * totalChars);
allChars.forEach((char, index) => {
if (!isScrollingDown && index >= currentCharIndex) {
if (colorTransitionTimers.has(index)) {
clearTimeout(colorTransitionTimers.get(index));
colorTransitionTimers.delete(index);
}
completedChars.delete(index);
gsap.set(char, { color: colorInitial });
return;
}
if (completedChars.has(index)) return;
if (index <= currentCharIndex) {
gsap.set(char, { color: colorAccent });
if (!colorTransitionTimers.has(index)) scheduleFinalTransition(char, index);
} else {
gsap.set(char, { color: colorInitial });
}
});
lastScrollProgress = progress;
},
});
});
}
function initTerminalRevealOnce(selector = "[data-terminal-reveal-once]") {
document.querySelectorAll(selector).forEach((element) => {
const colorInitial = element.dataset.colorInitial || "#dddddd";
const colorAccent = element.dataset.colorAccent || "#abff02";
const colorFinal = element.dataset.colorFinal || "#000000";
const duration = parseFloat(element.dataset.duration) || 1;
const stagger = parseFloat(element.dataset.stagger) || 0.02;
const wordSplit = SplitText.create(element, { type: "words", wordsClass: "word" });
const charSplit = SplitText.create(wordSplit.words, { type: "chars", charsClass: "char" });
const allChars = charSplit.chars;
gsap.set(allChars, { color: colorInitial });
element.classList.add("is-ready");
const tl = gsap.timeline({ paused: true });
allChars.forEach((char, index) => {
const charTl = gsap.timeline();
charTl.to(char, { color: colorAccent, duration: duration * 0.1, ease: "power2.out" });
charTl.to(char, { color: colorFinal, duration: duration * 0.3, ease: "power2.inOut" });
tl.add(charTl, index * stagger);
});
ScrollTrigger.create({
trigger: element,
start: "top 85%",
once: true,
onEnter: () => tl.play(),
});
});
}
document.addEventListener("DOMContentLoaded", () => {
initTerminalRevealScrub("[data-terminal-reveal]");
initTerminalRevealOnce("[data-terminal-reveal-once]");
});
</script>