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.
constandlet, nevervar.requestAnimationFramefor 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.
(() => {
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));
})();