In clean front-end code, data controls the ui, not the other way around.
Your JavaScript needs to be the source of truth in whatever you’re building. This rings true from the complexities of a Next.js application to a tabs section in a Webflow site.
So what do you do when your variable data changes? How can you update the ui once you’ve updated a variable?
That’s what reactivity solves. It provides convenient ways to listen for changes in data and run functions when they occur.
This article assumes the following:
I’m going to walk you through
Adding event listeners is something we do all the time. So what’s the hoo-haa about reactivity?
Why can’t we write
let foo = "bar"
foo.addEventListener('change', ()=>{ console.log('foo has changed')})
foo = "new"// console: "foo has changed"Because .addEventListener is a method provided by the DOM, for the DOM. Any DOM node (fancy term for HTML element) on your page has a selection of these methods available to it. They also have unique sets of events they can emit. Like “click”, “input”, “focus” and more.
Plain JavaScript variables do not have these methods attached. So we have to engineer our own way.
Frameworks like React, Svelte, Vue, etc…. Have this kind of reactive system built into them. But we’re Webflow developers. So what do we do?
We bring in our own.
In our case, that’s Vue Reactivity. An easy standalone framework that powers Vue.
In any given UI. We will have our state.
I’ll illustrate this with an example. A simple tabs section.
Our state will be the index that we’re currently on.
Quick aside to clear up vocab: State = data that explains how things should look to the user. If our state says index 1, then the user should see the 1st tab active.
However, there are many ways this state can be changed.
Without reactivity, we’d handle the data and UI changes within each event.
// user clicks a tab link
currentIndex = 0;
let tabLinks = document.querySelectorAll('.tab_link');
tabLinks.forEach((tab, index) => {
tab.addEventListener('click', () => {
currentIndex = index;
// change your ui here
tabLinks.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
});
// autoplay
setInterval(() => {
currentIndex = (currentIndex + 1) % tabLinks.length;
// change your ui here
tabLinks.forEach(t => t.classList.remove('active'));
tabLinks[currentIndex].classList.add('active');
}, 3000);
// keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') {
currentIndex++;
// change your ui here
tabLinks.forEach(t => t.classList.remove('active'));
tabLinks[currentIndex].classList.add('active');
}
});
See how easy it is for that to get messy? You’re changing the UI and the state inside each event. Making it much more challenging to add to this module with new events and triggers. Before you know it, you’re in a web of spaghetti code trying to figure out what’s caused a change.
This same example using Vue would be
// state
const currentIndex = ref(0);
const tabLinks = document.querySelectorAll('.tab_link');
// watch the index — when it changes, update the UI
watch(currentIndex, (index) => {
updateUI(index);
});
// user clicks a tab
tabLinks.forEach((tab, index) => {
tab.addEventListener('click', () => {
currentIndex.value = index;
});
});
// autoplay
setInterval(() => {
currentIndex.value =
(currentIndex.value + 1) % tabLinks.length;
}, 3000);
// keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') {
currentIndex.value++;
}
});
// change your ui here
function updateUI(index) {
// change your ui here
tabLinks.forEach(t => t.classList.remove('active'));
tabLinks[index]?.classList.add('active');
}That’s much simpler to understand what’s happening
Whenever the index changes, we update the ui, making this much easier for us to add to. If you wanted to add more functionality, you’d just make sure that it changes the currentIndex function, and reactivity will handle the rest.
When we create a reactive variable, we’re essentially wrapping that variable in an object to create a proxy.
So now whenever you change your proxy variable, it hijacks the native set and get functionality and does some new and wonderful things for us.
Explaining proxy objects and what exactly is being hijacked is beyond the scope of this article. I do think it’s important to know that you’re essentially wrapping your data with some extra functionality.
I’m glad you asked.
ref() is a function that creates your reactive variable. You can access the value using the .value property.
The end.
…
Is it more nuanced than that?
A little.
But not much.
let state = ref(0) wraps our value (0) in an object that we can access using .value
let state = ref(0)
state.value // 0We can also do that with an object.
let state = ref({
count: 1,
name: 'james'
})
state.value.name // 'james'Quick aside: We can also use reactive(). But from my experience, it’s basically the same with slight nuances. For the sake of this article, I think it’s easier to just use ref(), but if you want to know the differencee read this
A reactive variable on its own is pretty useless though. We’ve just made it marginally more complicated to access our data.
Now we want to listen for changes to our variable and do fun stuff.
I present…. watch()
Man I love it when names are easy to understand.
The watch() function will watch your variable and run functionality when it sees a change.
It takes two arguments.
let state = ref({
count: 1,
name: 'james'
});
console.log(state.value.count); // 0
watch(state, (oldValue, newValue) => {
console.log('going from ', oldValue, 'to ', newValue);
});
state.value = 1; // our watch() function will trigger nowYou can also listen to specific props inside a reactive variable. To do that, in your first argument you need to pass through a function that returns the values you want. You can pass as many as you want in an array like the example below.
const state = ref({
count: 1,
name: 'james'
});
// only track `count`
watch(
() => {
// function instead of the variable, but it returns the values we're watching
return [state.value.count];
},
([newCount], [oldCount]) => {
console.log(oldCount, '→', newCount);
}
);
state.value.count = 2; // triggers watch
state.value.name = 'bob'; // does NOT trigger watchA helpful technique if you’re listening to specific values, they don’t have to be from the same reactive variable.
const count = ref({
count: 1
});
const name = ref({
name: 'james'
});
// watch specific values, even from different refs
watch(
() => {
// function instead of the variable,
// but it returns the values we're watching
return [
count.value.count,
name.value.name
];
},
([newCount, newName], [oldCount, oldName]) => {
console.log('count:', oldCount, '→', newCount);
console.log('name:', oldName, '→', newName);
}
);
count.value.count = 2; // triggers watch
name.value.name = 'bob'; // also triggers watchNot going to bore you now. You’ve come so far!
To get you excited about why this skill is valuable, here’s a light collection of things that would benefit from using reactivity!
So you’ve learnt what reactivity is, why we need it, how to make a reactive variable, how to listen to changes on that variable and a handful of use cases to use this.
I’ve created 3 Webflow cloneables for you to check out. Each one is slightly more complex than the last, slowly incorporating features of reactivity. Check them out here.
Hono.js replaces messy if/else chains with clean, readable routing to build a reverse proxy that scales
In the next ~30 minutes, you'll have two different sites running under one URL.
Reverse proxies are the gateway drug to Webflow Enterprise. Almost every enterprise project I’ve been involved in has implemented it in some form.
Build an automated sync between Webflow's CMS and Algolia's search service using Cloudflare Workers.
If you’ve ever used Webflow’s native background video component and thought “damn, that looks rough” I'm here for you.
As more companies move to Webflow and demand for Webflow Enterprise grows, you’ll see more teams leaning on reverse proxies to solve some of Webflow’s infrastructure limitations.
A small keyboard shortcut can make a marketing site feel faster, more intentional, and “app-like” with almost no extra design or development
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.
Exploring ways to keep JavaScript modular and maintainable in Webflow — from Slater to GitHub to a custom window.functions pattern. A look at what’s worked (and what hasn’t) while building more scalable websites.