Skip to content

The Webflow rulebook

JavaScript rules

Script has to survive a designer renaming classes in Webflow, and it has to leave the canvas rendering correctly. Both shape the rules below.

Shape of the code

  • Vanilla ES6+. Wrap it in an arrow IIFE so nothing leaks: (() => { … })();
  • Arrow functions only — no declarations, no function expressions.
  • const and let, never var.
  • requestAnimationFrame for animation loops.

Select through data attributes

Classes belong to the designer after import and may be renamed. Selection binds to data-ht-* attributes instead, which survive.

Rejected

document.querySelectorAll('.button')
document.getElementById('button')

Correct

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

Read values from dataset (target.dataset.htData), not getAttribute.

State goes on attributes, not classList

Never use classList.add, remove or toggle to carry state. Webflow's canvas renders from the class list, so a class toggled at runtime makes the element render wrongly in the Designer.

Rejected

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

Correct

panel.dataset.open = String(next);
trigger.setAttribute('aria-expanded', String(next));

Use data-state, data-open, aria-expanded or aria-hidden, and keep aria-current accurate for accessibility.

The animation opacity rule

Never set opacity: 0 in CSS for an element you intend to animate in. Webflow's canvas honours it, so the designer opens the page and finds the section invisible.

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

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

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