Skip to content

The Webflow rulebook

JavaScript rules

JavaScript is what makes a section do things — open a menu, switch a tab, fade in on scroll. Two things shape these rules: a page in Webflow runs many sections' scripts side by side, and a designer will rename classes after you hand the page over.

The rules

Plain JavaScript, no libraries

Not auto-checked

Scripts are written in plain, modern JavaScript. No jQuery, React, GSAP or any other library that has to be loaded from somewhere else. (Loading an outside script file is checked automatically — see HTML rules.)

Why

A section has to work on its own once it is in Webflow. If it relies on a library, the page needs that library too — and if it is missing, or a different version, the section breaks. Modern browsers can already do everything these sections need.

Think of it like

A toy that needs a special charger. Lose the charger and the toy is useless. A toy that takes ordinary batteries works anywhere.

Each section's script is sealed in its own wrapper

Not auto-checked

All of a section's code sits inside (() => { … })();. This wrapper runs the code immediately and keeps everything defined inside it private to that section.

Why

Every section on a page shares one space for names. If two sections each create something called menu, the second silently replaces the first and one of them stops working. The wrapper gives each section its own private space.

Think of it like

Each family's letters in their own sealed envelope, rather than every family's pages loose in one sack.

javascript
(() => {
  const toggle = document.querySelector('[data-ht-menu-toggle]');
  // … everything for this section lives here
})();

const and let, never var

Checked automatically

Values are stored with const (for things that do not change) or let (for things that do). The old keyword var is never used.

Why

var ignores the boundaries you would expect: a value created inside one block leaks out of it, and it can be declared twice without any warning. With many scripts on one page, that is how one section accidentally overwrites another's data. const and let stay where they are put.

Think of it like

A label that can peel off one box and stick itself onto another without anyone noticing, versus one that stays where you stuck it.

Rejected

var count = 0;

Correct

let count = 0;
const button = document.querySelector('[data-ht-counter]');

Arrow functions only

Checked automatically

Functions are written as arrow functions — const open = () => { … }; — not with the function keyword.

Why

A function written with function can end up visible to the whole page and clash with another section's function of the same name. Arrow functions stored in const stay inside the section's wrapper. One consistent style also makes every section's code read the same way.

Think of it like

Everyone in the office filing documents the same way. Not because the other way is wrong, but because then anyone can find anything.

Rejected

function openMenu() { … }

Correct

const openMenu = () => { … };

Find elements by their data-ht-* attribute

Checked automatically

A script finds the elements it works with through their data-ht-* attribute, e.g. document.querySelectorAll('[data-ht-tab]'). It never searches by class (.tab) or id (#tab), and never uses getElementById, getElementsByClassName or getElementsByTagName.

To read a value stored on an element, use dataset: tab.dataset.htTab.

Why

After import, classes belong to the designer, and renaming them is part of their normal work in Webflow. A script that looked for .tab finds nothing after the rename, and the tabs stop working without any error message. The data-ht-* attribute is not touched when classes change.

Think of it like

Looking for a friend in a crowd by their name badge rather than their red shirt. They may change their shirt; the badge still says who they are.

Rejected

document.querySelectorAll('.faq__question');
document.getElementById('faq');

Correct

document.querySelectorAll('[data-ht-faq-question]')
  .forEach((question) => { … });

Open, closed, active: state lives on attributes, not classes

Checked automatically

When something changes state — a menu opens, a tab becomes active — the script changes an attribute such as data-open, data-state, aria-expanded or aria-hidden. It does not add or remove classes with classList.add, remove or toggle. The CSS then styles each state through the attribute.

Why

Webflow builds its Designer from the classes it knows about. A class that only appears while the page is running is one the designer never created and cannot see, so the element looks wrong in the Designer and the designer cannot style its “open” look.

Attributes like aria-expanded have a bonus: screen readers announce them, so blind visitors also hear that the menu is now open.

Think of it like

A traffic light. To tell cars to stop you change the light — you do not repaint the cars red.

Rejected

panel.classList.toggle('is-open');

Correct

panel.dataset.open = String(next);
trigger.setAttribute('aria-expanded', String(next));
css
.faq__panel { display: none; }
.faq__panel[data-open="true"] { display: block; }

Hide things for animation from JavaScript, not CSS

Checked automatically

An element that should fade in as you scroll is marked with data-ht-animate. The script hides it when the page loads and shows it when it scrolls into view. The CSS never contains opacity: 0.

Why

Webflow's Designer uses your CSS but does not run your scripts. If the CSS hid the element, it would stay hidden in the Designer forever — the designer would open the page and find a blank space where the content should be. Hidden by the script, the element is visible in the Designer and still animates on the live site.

Think of it like

A stage curtain. Sew it shut and even the crew rehearsing cannot see the stage. Have a stagehand close it just before the show starts, and the crew can work while the audience still gets the reveal.

javascript
(() => {
  const items = document.querySelectorAll('[data-ht-animate]');
  items.forEach((item) => { item.style.opacity = '0'; });

  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;
      entry.target.style.opacity = '1';
      observer.unobserve(entry.target);
    });
  }, { threshold: 0.1 });

  items.forEach((item) => observer.observe(item));
})();

Smooth animation loops use requestAnimationFrame

Not auto-checked

Anything that updates many times a second — a counter, a parallax effect — runs inside requestAnimationFrame, not a timer like setInterval.

Why

requestAnimationFrame runs exactly when the screen is about to redraw, so motion is smooth, and it pauses on its own when the tab is in the background, so it does not drain a laptop's battery. A timer runs whether or not anyone is looking.

Think of it like

Dancing to the beat of the music rather than to your own stopwatch.