Keeping JavaScript clean and maintainable inside Webflow can be… tricky. Over the last few projects, I’ve been exploring different ways to modularise my scripts — not just to stay sane, but to make things clearer for other developers (and future me) down the line.
In this article my goal is to explain the concepts and uses cases as I explored them
As projects grow in complexity — especially ones that lean heavily on GSAP for interactions — your codebase can get real messy, real fast. Think of a slider that uses Flip
, InertiaPlugin
, Observer
, maybe even some ScrollTrigger
magic. It’s not unusual to rack up a few hundred lines of JavaScript just to get something polished and accessible.
Now Webflow technically allows up to 10,000 characters per embed, but cramming everything into one block just feels wrong. What I wanted was something closer to the structure I’m used to in VS Code — small files, reusable utilities, named exports. Something clean.
I cycled through a few approaches trying to find the right balance between flexibility, maintainability, and Webflow-friendliness:
pageFunctions
Pattern: This one I really liked — especially on older Lumos setups. But the trade-off was losing the ability to easily import/export functions across files. That modularity is something I’ve come to love. It's since been removed from newer versions of LumosWhat if we treat window
as a kind of shared namespace?
Finsweet already does a similar approach with Attributes. If you have any attribute on the page, open the console and type fsAttributes. You'll get an object with data about the attributes that are on the page.
That was the inspiration for this approach. Passing functions around different embeds so we could keep things nice and tidy.
Let’s say you have a changeSlide()
function that handles a GSAP timeline. In a typical setup, you’d keep it in its own file and import it into something like initSlider.js
. Why not mimic that in Webflow by doing:
window.functions = window.functions || {};
window.functions.changeSlide = () => {
// GSAP stuff here
};
Then, in another embed:
window.functions.initSlider = () => {
nextBtn.addEventListener('click', window.functions.changeSlide);
};
It’s not quite the same as proper module imports, but it gives you some of that clarity back — and avoids bloating any one embed.
This brings up the tricky part: timing.
Webflow scripts execute in order as they appear in the DOM, but there’s no real guarantee that your changeSlide
embed will run before your initSlider
one unless you wrap everything in the right event listeners.
What I started doing was splitting my embeds into two types:
window.functions
. No immediate execution.To make this bulletproof, I experimented with dispatching custom events:
// In the module embed
window.functions = window.functions || {};
window.functions.changeSlide = () => { ... };
document.dispatchEvent(new CustomEvent('module:slider:ready'));
// In the init embed
document.addEventListener('module:slider:ready', () => {
window.functions.initSlider();
});
This way, each piece of functionality can load independently and safely hook into the right moment.
It depends.
For heavier components like a complex slider or custom GSAP sequence, this structure makes life easier. If you’re building a one-off interaction or a smaller effect, it’s absolutely overkill.
Something else to keep in mind is the skill level of the team that will maintain the project. The goal of this is to make the code more readable and maintainable. But if you’re handing it off to a non-technical team or a junior Webflow developer, it’s best to think hard about if this is the right approach and if you do decide to go ahead with it, comment the code extremely clearly.
Overall what I’ve found works best is a hybrid: use the event-based structure for large features that need clear separation, and keep smaller stuff self-contained.
Good projects get messy. Great projects get cleaned up. How should we handle technical debt and real-world refactoring?
A practical, code-heavy dive into GSAP’s utility functions—keyframes, pipe, clamp, normalize, and interpolate—and why they’re so much more than just shortcuts for animation math.
GSAP lets you pass _functions_ as property values. I've known this for a while but never really explored it particularly deeply. Over the last couple of weeks I've been testing, experimenting and getting creative with it to deepen my understanding.