Skip to content

JavaScript Basics

At Terra, JavaScript is organized around handlers that manage component lifecycle, async imports for performance, and SWUP for smooth page transitions. This section covers the essentials you need to build interactive features.


Handlers are controller classes that bridge the framework and component instances. They manage when and how components are initialized and destroyed across page transitions.

In projects with SWUP page transitions, components need to:

  • Re-initialize on every page navigation (new DOM elements)
  • Destroy properly before transitions (prevent memory leaks)
  • Load efficiently (only when needed, not all upfront)

Handlers solve these problems automatically.


SWUP is a page transition library that enables smooth navigation without full page reloads. Handlers listen to events hooked up to SWUP’s lifecycle to know when to create and destroy components.

// After new page content is loaded
this.emitter.on("MitterContentReplaced", async () => {
// Initialize components for new page
});
// Before transitioning to new page
this.emitter.on("MitterWillReplaceContent", () => {
// Destroy existing components
});

Flow:

  1. User clicks a link
  2. SWUP fetches new page content
  3. MitterWillReplaceContent fires → Destroy old components
  4. Content is replaced
  5. MitterContentReplaced fires → Initialize new components

Let’s create a handler for a simple accordion component using Collapsify.

Use our handler template at src/js/handler/_handlerFolder/Handler.js, copy and paste it in the corresponding folder for your new library.

File: src/js/handler/collapsify/Handler.js

Understand how to build a handler and what everything does in detail here.

import CoreHandler from "../CoreHandler";
import { updateScrollTriggers } from "@js/utilities/updateScrollTriggers";
/**
* Collapsify Handler
*/
class Handler extends CoreHandler {
constructor(payload) {
super(payload);
// Shared callbacks for all configurations
this.callbacks = {
onComplete: () => {
// Update scroll triggers after collapsify is initialized
updateScrollTriggers({ Manager: this.Manager });
},
onSlideEnd: (isOpen, contentID) => {
// Update scroll triggers after collapsify slide ends
updateScrollTriggers({ Manager: this.Manager });
},
};
// Generic configuration
this.configSimple = ({ element }) => ({
element,
...this.callbacks,
});
// Accordion version
this.configAccordion = ({ element }) => ({
element,
closeOthers: true,
nameSpace: "accordion",
...this.callbacks,
});
// Tabs version with dropdown
this.configTabs = ({ element }) => ({
element,
isTab: true,
nameSpace: "tab",
dropdownElement: element.querySelector(".c--tabs-a__hd__wrapper__item"),
...this.callbacks,
});
this.init();
this.events();
}
get updateTheDOM() {
return {
collapsifyElement: document.querySelectorAll(`.js--collapsify`),
collapsifyAccordion: document.querySelectorAll(`.js--collapsify-accordion`),
collapsifyTab: document.querySelectorAll(`.js--collapsify-tab`),
};
}
init() {
super.getLibraryName("Collapsify");
}
events() {
this.emitter.on("Collapsify:load", async () => {
await super.assignInstances({
elementGroups: [
{
elements: this.DOM.collapsifyElement,
config: this.configSimple,
boostify: { distance: 30 },
},
{
elements: this.DOM.collapsifyAccordion,
config: this.configAccordion,
boostify: { distance: 30 },
},
{
elements: this.DOM.collapsifyTab,
config: this.configTabs,
boostify: { distance: 30 },
},
],
forceLoad: true,
});
});
this.emitter.on("MitterContentReplaced", async () => {
this.DOM = this.updateTheDOM;
await super.assignInstances({
elementGroups: [
{
elements: this.DOM.collapsifyElement,
config: this.configSimple,
boostify: { distance: 30 },
},
{
elements: this.DOM.collapsifyAccordion,
config: this.configAccordion,
boostify: { distance: 30 },
},
{
elements: this.DOM.collapsifyTab,
config: this.configTabs,
boostify: { distance: 30 },
},
],
});
});
this.emitter.on("MitterWillReplaceContent", () => {
if (this.DOM.collapsifyElement.length) {
super.destroyInstances();
}
});
}
}
export default Handler;

Key parts:

  • updateTheDOM - Returns fresh DOM queries
  • init() - Sets library name
  • events() - Listens for SWUP transitions
  • MitterContentReplaced - Creates instances for new page
  • MitterWillReplaceContent - Destroys instances before transition

File: src/scripts/Main.js

import CollapsifyHandler from "@scripts/handler/collapsify/Handler";
class Main extends Core {
async init() {
super.init();
// Initialize the handler
new CollapsifyHandler({
...this.handler,
name: "CollapsifyHandler",
});
// ... other handlers
}
}

Learn more about the Main file here

File: src/js/resources.js

export const getModules = () => {
return [
{
name: "Collapsify",
resource: async () => {
const { default: Collapsify } = await import("@terrahq/collapsify");
return Collapsify;
},
options: {
modifyHeight: true,
},
},
// ... other modules
];
};

Understand how our resources file works here

Benefits of async imports:

  • ✅ Smaller initial bundle
  • ✅ Faster page load
  • ✅ Only loads when needed

How async imports work:

// Traditional import - loads immediately
import Collapsify from "@terrahq/collapsify";
// Async import - loads when needed
const { default: Collapsify } = await import("@terrahq/collapsify");

Performance impact:

Before async imports:

bundle.js: 500KB (includes all libraries) → Slow initial load

After async imports:

bundle.js: 100KB (core only)
collapsify.js: 50KB (loads when needed)
fadeIn.js: 30KB (loads when needed)
→ Fast initial load + on-demand loading

Let’s create a custom animation class using GSAP (GreenSock Animation Platform).

GSAP is a powerful JavaScript animation library we use for smooth, performant animations. It provides fine-tuned control over timing, easing, and sequencing.

📚 GSAP Documentation.

Why we use GSAP:

  • High performance
  • Precise control
  • Timeline management
  • Works everywhere (including mobile)

File: src/scripts/handler/fadeIn/FadeIn.js

import gsap from "gsap";
/**
* FadeIn Class
*
* Creates a smooth fade-in animation on scroll using GSAP.
* Elements start invisible and fade in when they enter the viewport.
*
* Parameters:
* - element (DOM Element): The element to animate
* - duration (number): Animation duration in seconds (default: 0.8)
* - delay (number): Delay before animation starts (default: 0)
* - yOffset (number): Vertical offset for slide effect (default: 30)
*
* Example Usage:
* ```javascript
* new FadeIn({
* element: document.querySelector('.js--fade-in'),
* duration: 1,
* delay: 0.2,
* yOffset: 40
* });
* ```
*/
class FadeIn {
constructor({ element, duration = 0.8, delay = 0, yOffset = 30 }) {
this.DOM = {
element: element,
};
this.duration = duration;
this.delay = delay;
this.yOffset = yOffset;
this.init();
this.events();
}
init() {
// Set initial state (invisible, offset down)
gsap.set(this.DOM.element, {
opacity: 0,
y: this.yOffset,
});
// Create the animation timeline (paused)
this.timeline = gsap.timeline({ paused: true });
this.timeline.to(this.DOM.element, {
opacity: 1,
y: 0,
duration: this.duration,
delay: this.delay,
ease: "power2.out",
});
}
events() {
// Create Intersection Observer to trigger on scroll
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.play();
this.observer.unobserve(entry.target); // Animate once
}
});
},
{ threshold: 0.2 } // Trigger when 20% visible
);
this.observer.observe(this.DOM.element);
}
play() {
if (this.timeline) {
this.timeline.play();
}
}
destroy() {
// Disconnect observer
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
// Kill GSAP timeline
if (this.timeline) {
this.timeline.kill();
this.timeline = null;
}
// Clear references
this.DOM = null;
this.duration = null;
this.delay = null;
this.yOffset = null;
}
}
export default FadeIn;

This class demonstrates:

  • ✅ Proper this.DOM structure
  • ✅ GSAP timeline creation
  • ✅ Intersection Observer for scroll triggering
  • ✅ Complete destroy() method
  • ✅ Full documentation

File: src/scripts/handler/fadeIn/Handler.js

import CoreHandler from "../CoreHandler";
class FadeInHandler extends CoreHandler {
constructor(payload) {
super(payload);
this.init();
this.events();
// Dynamic config based on data attributes
this.config = (element) => ({
duration: parseFloat(element.dataset.duration) || 0.8,
delay: parseFloat(element.dataset.delay) || 0,
yOffset: parseInt(element.dataset.yOffset) || 30,
});
}
get updateTheDOM() {
return {
fadeInElements: document.querySelectorAll(".js--fade-in"),
};
}
init() {
super.init();
super.getLibraryName("FadeIn");
}
events() {
super.events();
this.emitter.on("MitterContentReplaced", async () => {
this.DOM = this.updateTheDOM;
this.Manager.instances.FadeIn = [];
super.assignInstances({
elementGroups: [
{
elements: this.DOM.fadeInElements,
config: this.config, // Callback for per-element config
boostify: { distance: 50 },
},
],
});
});
this.emitter.on("MitterWillReplaceContent", () => {
if (this.DOM.fadeInElements.length) {
super.destroyInstances({ libraryName: "FadeIn" });
}
});
}
}
export default FadeInHandler;
<!-- Simple fade-in with defaults -->
<div class="c--banner-a js--fade-in">
<h2 class="c--banner-a__title">This fades in on scroll</h2>
</div>
<!-- Custom animation settings via data attributes -->
<div class="c--banner-a js--fade-in" data-duration="1.2" data-delay="0.3" data-y-offset="50">
<h2 class="c--banner-a__title">Custom fade-in animation</h2>
</div>

gsap.to(element, {
opacity: 1,
duration: 0.6,
ease: "power2.out",
});
gsap.to(".js--card", {
opacity: 1,
y: 0,
stagger: 0.1, // 0.1s delay between each
duration: 0.6,
});
const tl = gsap.timeline();
tl.to(".js--title", { opacity: 1, duration: 0.6 })
.to(".js--subtitle", { opacity: 1, duration: 0.6 }, "-=0.3") // Overlap
.to(".js--button", { opacity: 1, y: 0, duration: 0.6 });
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
gsap.to(".js--element", {
opacity: 1,
scrollTrigger: {
trigger: ".js--element",
start: "top 80%", // When top of element hits 80% of viewport
end: "bottom 20%",
scrub: true, // Animate with scroll
},
});

Always extend CoreHandler:

class MyHandler extends CoreHandler {}

Destroy instances on page transition:

this.emitter.on("MitterWillReplaceContent", () => {
if (this.DOM.collapsifyElement.length) {
super.destroyInstances();
}
});

Include complete destroy() methods:

destroy() {
if (this.timeline) {
this.timeline.kill();
this.timeline = null;
}
this.DOM = null;
}

Use data attributes for configuration:

this.config = (element) => ({
speed: parseInt(element.dataset.speed) || 400,
});

Don’t create instances manually:

// ❌ Wrong
new Slider({ element });
// ✅ Correct - let handler manage it
super.assignInstances({ elementGroups: [...] });

Don’t forget both SWUP events:

// ❌ Missing - will cause memory leaks
this.emitter.on("MitterContentReplaced", async () => {});
// ✅ Complete - both events handled
this.emitter.on("MitterContentReplaced", async () => {});
this.emitter.on("MitterWillReplaceContent", () => {});

Don’t skip destroy() in classes:

// ❌ Wrong - will leak memory
class MyClass {
constructor({ element }) {}
// Missing destroy()
}
// ✅ Correct - cleanup included
class MyClass {
constructor({ element }) {}
destroy() {
this.DOM = null;
}
}

JavaScript at Terra is organized around:

  • Handlers - Manage component lifecycle across SWUP transitions
  • Async Imports - Optimize performance with code splitting
  • GSAP - Create smooth, performant animations
  • SWUP Events - Know when to initialize and destroy components

By following these patterns, your components will:

  • ✅ Load efficiently
  • ✅ Work across page transitions
  • ✅ Prevent memory leaks
  • ✅ Provide smooth animations

This was only a summary and quick reference guide on how we work. Understand our framework and how we work with JavaScript in our JS section

Knowledge Check

Test your understanding of this section

Loading questions...