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.
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:
// 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:
// 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.
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.
Related
- Animation —
.animate/.is-animated, the archetypal state pair. - SCSS architecture — the colocated
.scssthat owns each state's look. - Traits — how
FadesInandAnimatesOnScrollattach these behaviours to a host.