Theme Framework How the LeaseLeads theme frontend works

The State-Class Contract

JS toggles state classes; the colocated SCSS owns every visual expression keyed off them.

On this page 3

There is one hard rule about the JS/CSS boundary in this theme, and it holds everywhere:

JS drives state through classes. It does not define styles. The colocated SCSS owns all visual expression, keyed off those classes.

A behaviour's JavaScript decides what state the element is in — open, sticky, animated, settled, loaded — and expresses that decision as a state class it toggles. It never sets style.color, never writes a transition, never animates a value. The component's .scss reads the class and owns every pixel of what that state looks like.

Important

This is not "don't write JS." Authoring JavaScript is expected and normal — you write the observers, the event handlers, the logic that decides state. The rule is narrow: that JS must not define styles. It toggles a class; the SCSS does the rest. (A handful of unavoidable inline hooks exist — clearing an opacity set for a no-JS fallback — but they clear values, they don't design.)

The real class list

These are the state classes the codebase actually toggles. Recognise them; when you see one in JS, the styling for it lives in the matching component's SCSS:

Class Meaning Toggled by
is-animated Scrolled into view, play the reveal Animation component
loaded Module / image content is ready to show FadesIn trait, Image
is-sticky Header is scrolled past the top HeaderBar
is-open Accordion item / submenu is expanded Accordion, SubMenu
is-active Filter / marker / tab is the selected one Filters, AmenityHighlights
is-settled / is-introduced Media effect has settled / introduced MediaEffect
is-muted Video control reflects muted audio VirtualToursCarousel
is-filtered Gallery has an active filter MasonryGallery

A worked example

The header's JS watches scroll and decides one thing — sticky or not — and says so with a class:

js
// layouts/HeaderBar/HeaderBar.js
toggleClass() {
  window.scrollY
    ? manipulateDOM(() => this.element.classList.add('is-sticky'))
    : manipulateDOM(() => this.element.classList.remove('is-sticky'))
}

No colours, no timings, no transitions — just the state. The colocated SCSS owns the entire visual difference between sticky and not:

scss
// layouts/HeaderBar/HeaderBar.scss
&.is-sticky .header-bar__bar--transparent {
  display: none;
  opacity: 0;
  transition: opacity 0.25s ease-out, display 0.25s ease-out;
  transition-behavior: allow-discrete;
}

&.is-sticky .header-bar__bar--solid {
  display: flex;
  opacity: 1;
  transition: opacity 0.25s ease-out, display 0.25s ease-out;
}

The same shape appears with .module.loaded fading a Module in, .image.loaded crossing a preload placeholder out, and .animate.is-animated revealing on scroll — different states, identical contract.

Note

Why the split. Keeping visual expression entirely in SCSS means a designer can retheme any state — change the sticky header's transition, the fade duration, the open accordion's look — by editing one colocated stylesheet, with zero risk of touching behaviour. It also keeps JS diffs about logic and SCSS diffs about appearance, so the two can move independently. Toggle state; never style from JS.

  • Animation.animate / .is-animated, the archetypal state pair.
  • SCSS architecture — the colocated .scss that owns each state's look.
  • Traits — how FadesIn and AnimatesOnScroll attach these behaviours to a host.