
Documentation
Webflow
Code
Setup: External Scripts
External Scripts in Webflow
Make sure to always put the External Scripts before the Javascript step of the resource.
In this video you learn where to put these in your Webflow project? Or how to include a paid GSAP Club plugin in your project?
HTML
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/SplitText.min.js"></script>
Step 1: Copy structure to Webflow
Copy structure to Webflow
In the video below we described how you can copy + paste the structure of this resource to your Webflow project.
Copy to Webflow
Webflow structure is not required for this resource.
Step 1: Add HTML
HTML
<div class="demo-group">
<div class="ghost-section">
<h1 data-load-skeleton="dark" class="ghost-heading">This heading will reveal with an effect called a ‘skeleton loader’.</h1>
</div>
<div class="ghost-section is--light">
<h1 data-load-skeleton="light" class="ghost-heading">→ Fully attribute based<br>→ Set different themes<br>→ Control skeleton styling<br></h1>
</div>
<div class="ghost-section">
<h1 data-load-skeleton="dark" class="ghost-heading is--small">The idea and concept of Skeleton Loading was introduced in 2013 by Luke Wroblewski. It describes the concept of a blank screen where dynamic content is replaced by styled blocks (skeleton) and is replaced with real content once it's finished loading.</h1>
</div>
</div>
HTML structure is not required for this resource.
Step 2: Add CSS
CSS
.ghost-section {
grid-column-gap: 2em;
grid-row-gap: 2em;
flex-flow: column;
justify-content: center;
align-items: center;
width: 100%;
min-height: 100vh;
padding-left: 1em;
padding-right: 1em;
display: flex;
}
.ghost-section.is--light {
color: #121422;
background-color: #cdf7ff;
}
.ghost-heading {
letter-spacing: -.03em;
text-transform: uppercase;
max-width: 15em;
margin-top: 0;
margin-bottom: 0;
font-family: RM Mono, Arial, sans-serif;
font-size: 4em;
font-weight: 400;
line-height: 1;
}
.ghost-heading.is--small {
max-width: 25em;
font-size: 2.5em;
line-height: 1.1;
}
/* Define color themes here */
[data-load-skeleton="dark"]{
--color-skeleton-base: #2E3643;
--color-skeleton-pulse: #53636F;
}
[data-load-skeleton="light"]{
--color-skeleton-base: #B1D5DE;
--color-skeleton-pulse: #8CA8B2;
}
/* Hide actual text line so that its not visible underneath the placeholder div */
[data-load-skeleton] .single-line{
visibility: hidden;
}
/* Style your placeholder/skeleton div over here */
.skeleton-overlay{
position: absolute;
top: 50%;
transform: translate(0px, -50%);
left: 0px;
width: 100%;
height: 80%;
border-radius: 0.25rem;
z-index: 1;
background-color: var(--color-skeleton-base);
}
Step 2: Add custom Javascript
Custom Javascript in Webflow
In this video, Ilja gives you some guidance about using JavaScript in Webflow:
Step 2: Add Javascript
Step 3: Add Javascript
Javascript
gsap.registerPlugin(ScrollTrigger, SplitText)
function initSkeletonLoader() {
// —————— CLEANUP —————— //
function cleanup() {
document.querySelectorAll('[data-load-skeleton]').forEach(target => {
if (target.splitInstance) {
target.splitInstance.revert();
delete target.splitInstance;
}
target.querySelectorAll('.skeleton-overlay').forEach(overlay => overlay.remove());
});
ScrollTrigger.getAll().forEach(trigger => {
if (trigger.vars && trigger.vars.trigger && trigger.vars.trigger.closest('[data-load-skeleton]')) {
trigger.kill();
}
});
}
// —————— SPLIT TEXT —————— //
function split() {
let skeletonLoadTargets = document.querySelectorAll('[data-load-skeleton]');
skeletonLoadTargets.forEach(target => {
let splitInstance = new SplitText(target, {
type: "lines",
linesClass: "single-line",
});
target.splitInstance = splitInstance;
target.setAttribute("aria-label", target.textContent);
splitInstance.lines.forEach(line => {
line.setAttribute("aria-hidden", "true");
let wrapper = document.createElement('div');
wrapper.classList.add('single-line-wrap');
line.parentNode.insertBefore(wrapper, line);
wrapper.appendChild(line);
});
});
}
// —————— BUILD SKELETON —————— //
function build() {
const instances = document.querySelectorAll('[data-load-skeleton]');
instances.forEach(instance => {
const overlays = [];
const lineWrappers = instance.querySelectorAll('.single-line-wrap');
lineWrappers.forEach(wrapper => {
const overlay = document.createElement('div');
overlay.classList.add('skeleton-overlay');
wrapper.style.position = 'relative';
wrapper.appendChild(overlay);
overlays.push(overlay);
});
overlays.forEach((overlay, i) => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: overlay,
start: "top 90%",
once: true
},
defaults: {
duration: 0.5,
ease: "power2.inOut"
}
});
const pulseColor = gsap.getProperty(instance, "--color-skeleton-pulse");
const textEl = overlay.previousElementSibling;
tl.to(overlay, {
backgroundColor: pulseColor,
duration: 0.3,
ease: "power1.inOut",
repeat: 2,
yoyo: true,
delay: i * 0.05
})
.to(overlay, {
opacity: 0,
onComplete: () => overlay.remove()
})
.to(textEl, {
autoAlpha: 1
}, "<");
});
});
}
// —————— RUN ALL —————— //
function run() {
cleanup();
split();
build();
}
// —————— DEBOUNCE —————— //
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// —————— RESIZE HANDLER —————— //
let prevWidth = window.innerWidth;
const onResize = debounce(() => {
const currentWidth = window.innerWidth;
if (currentWidth !== prevWidth) {
prevWidth = currentWidth;
run();
}
}, 250);
window.addEventListener('resize', onResize);
// —————— KICK IT OFF ON INIT —————— //
run();
// Expose cleanup as return
return () => {
window.removeEventListener('resize', onResize);
cleanup();
};
}
// —————— INIT ON LOAD —————— //
document.addEventListener("DOMContentLoaded", () => {
document.fonts.ready.then(initSkeletonLoader);
});
Step 3: Add custom CSS
Step 2: Add custom CSS
Custom CSS in Webflow
Curious about where to put custom CSS in Webflow? Ilja explains it in the below video:
CSS
[data-load-skeleton="dark"]{
--color-skeleton-base: #2E3643;
--color-skeleton-pulse: #53636F;
}
[data-load-skeleton="light"]{
--color-skeleton-base: #B1D5DE;
--color-skeleton-pulse: #8CA8B2;
}
/* Hide actual text line so that its not visible underneath the placeholder div */
[data-load-skeleton] .single-line{
visibility: hidden;
}
/* Style your placeholder/skeleton div over here */
.skeleton-overlay{
position: absolute;
top: 50%;
transform: translate(0px, -50%);
left: 0px;
width: 100%;
height: 80%;
border-radius: 0.25rem;
z-index: 1;
background-color: var(--color-skeleton-base);
}
Implementation
The main thing here is adding a data-load-skeleton
attribute on a text element. Then, add an attribute value of your choice. This will define the color theme of your skeleton/overlay div blocks. For example, we've got data-load-skeleton="dark"
. Then in CSS, make sure to define 2 variables for each theme you might want to have on the site. Here's an example:
[data-load-skeleton="dark"]{
--color-skeleton-base: #2E3643;
--color-skeleton-pulse: #53636F;
}
The usage of GSAPs getProperty()
method makes sure that the GSAP tween to animate the color of our skeletons can pull the color from CSS, so no need to define that in JS too! The only values you might want to play around with is the duration, repetitions and easing of those skeletons. All of that is defined in the GSAP timeline inside of the initSkeletonLoader()
function. Have fun experimenting!
Best practices
We've included a bunch of best practices for this one. The main thing is our cleanup()
function. This one is called on resize of the window, to make sure ScrollTriggers are killed, overlays are removed, and the text split is undone. This resize listener is also debounced, meaning that there's a delay of 250ms (you can change this) between any resize event and the execution of our function. This just makes sure that when a user is resizing, our functions are not called in super frequently. When the resize is over, we just re-do all the skeleton loaders to account for any wrapping of text blocks etc.
Resource Details
Last updated
August 29, 2025
Type
The Vault
Category
Scroll Animations
Need help?
Join Slack