Theme Framework How the LeaseLeads theme frontend works

Animation

The lib/animate fade() mixin, driven by .animate + .is-animated and revealed on scroll by the Animation component.

On this page 4

Scroll-reveal animation is a two-part contract: SCSS declares how a thing fades in, and JS decides when by toggling one class. You write the SCSS; the framework supplies the JS.

The contract: .animate.is-animated

An element opts in by carrying the animate class in its markup. It starts hidden; when is-animated is added, it plays. The fade() mixin from lib/animate wires both states for you:

scss
// lib/animate.scss (excerpt)
@mixin fade($direction: none, $duration: 1s, $delay: 0s, $target: self, $easing: ease-out) {
  #{$selector} {          // e.g. &.animate
    animation-duration: $duration;
    animation-fill-mode: forwards;
    opacity: 0;
    // $direction sets a from-state translate (up / down / left / right)
  }

  #{$applied_selector} {  // e.g. &.animate.is-animated
    animation-name: fade;
  }
}

The mixin only ever sets the from-state (opacity 0, plus an optional directional translate) and names the animation once is-animated lands. The fade keyframe itself is emitted globally by Crucial.scss, so it ships eagerly and every mixin call just references it by name:

scss
// global/Crucial/Crucial.scss
@keyframes fade {
  to {
    opacity: 1;
    translate: 0 0;
  }
}

Who adds is-animated

The Animation component (components/Animation/Animation.js, selector .animate) observes each opted-in element with the shared global IntersectionObserver and adds the class the moment it scrolls into view:

js
// components/Animation/Animation.js
static selector = '.animate'

animateIn() {
  manipulateDOM(() => {
    this.element.classList.add('is-animated')
  })
}

A host discovers and lazy-loads it via the AnimatesOnScroll trait — mix that onto a Module and every .animate descendant inside it boots the Animation component automatically. The JS never touches a style; it only toggles the class the SCSS is keyed to.

Fading in a section

Give the element animate in the view, then call the mixin with $target: self:

scss
@use 'lib/animate' as animate;

.location-gallery {
  opacity: 0;
  @include animate.fade(up, $target: self);
}

That's it — hidden until it scrolls into view, then it fades up over its default duration. Use $direction (up, down, left, right, none) for the entry translate, $duration / $delay for timing, and $target to move the animated selector onto the element itself (self), its children (children), or a custom child.

Note

Why keyframe-global but mixin-local. A @keyframes can't be scoped, so emitting fade once in Crucial avoids every component redeclaring it. The mixin stays local because the from-state (how far and which way it translates, how long it takes) is a per-component design decision. The .animate / .is-animated split keeps the reveal trigger in JS and the reveal expression in SCSS.