Back Home
TOC Loading..
Keyframes
References
Github Repo
Webflow Cloneable

Managing UI State in Webflow with JavaScript

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. 

Prerequisites.

This article assumes the following:

  • You have a basic understanding of JavaScript
    • What’s a variable
    • What’s a function
    • What’s an object
    • Manipulating the DOM with event listeners and selectors
  • You’ve built some kind of complex UI before (Webflow or not)
    • Tabs
    • FAQs
    • Calculator
    • Anything that requires you to track data, even just the current index.
  • You want to handle these kinds of builds consistently.

In this Article

I’m going to walk you through

  1. Why can’t you write .addEventListener on your variables?
  2. Why we want our data to control our ui
  3. What reactivity does for us
  4. How to create reactive variables
  5. How to listen for changes on those variables
  6. Use cases for this technique

Why can’t you write .addEventListener on your variables?

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.

Why do we want data to control our frontend?

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. 

  1. The user clicks a tab link.
  2. The tab autoplays
  3. The user navigates with their keyboard

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. 

What is reactivity doing for us?

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.

How do we create a reactive variable?

I’m glad you asked.

Ref()

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 // 0

We 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.

How to listen for changes in those variables

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. 

  1. The reactive variable (or a function that returns an array of reactive variables)
  2. The function you want to run when that variable changes.
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 now

You 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 watch

A 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 watch

Use cases

Not 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! 

  • Persisting state (e.g. saving to localStorage)
  • Triggering API calls when inputs change
  • Running form validation as fields change
  • Sending analytics or tracking events
  • Responding to external data (WebSockets, Firebase, Supabase)
  • Tabs, accordions, FAQs, index-based UI
  • Managing page transitions
  • Filters & sorting (finsweet filters use Vue reactivity)
  • Advancing stages in multi-step forms
  • Syncing pricing pages (currency, plan, timeframe)

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.

  1. Simple number counter
  2. Matching text inputs to the ui
  3. Object based reactivity demo
Writings

Using HonoJS with your reverse proxy

Hono.js replaces messy if/else chains with clean, readable routing to build a reverse proxy that scales

Reverse Proxy First Steps

In the next ~30 minutes, you'll have two different sites running under one URL.

Webflow Reverse Proxy Overview: What It Is, Why It Matters, and When to Use It

Reverse proxies are the gateway drug to Webflow Enterprise. Almost every enterprise project I’ve been involved in has implemented it in some form.

Building a Webflow to Algolia Sync with Cloudflare Workers

Build an automated sync between Webflow's CMS and Algolia's search service using Cloudflare Workers.

Using Videos Effectively in Webflow (Without Losing Your Mind)

If you’ve ever used Webflow’s native background video component and thought “damn, that looks rough” I'm here for you.

Webflow + Cloudflare reverse proxy. Why and How

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.

How (and why) to add keyboard shortcuts to your Webflow site

A small keyboard shortcut can make a marketing site feel faster, more intentional, and “app-like” with almost no extra design or development

Useful GSAP utilities

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.

Using Functions as Property Values in GSAP (And Why You Probably Should)

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.

Organising JavaScript in Webflow: Exploring Scalable Patterns

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.

Building a Scroll-Based Image Sequencer with GSAP

An exploration in building a scroll-driven image sequence animation using GSAP and HTML5 canvas. Using Avif file compression with the Avif CLI, hosting strategies (Webflow vs AWS), GSAP and the quirks of working with canvas.