Skip to content

Js Classes

JavaScript classes provide a structured way to create reusable components with shared behavior and state. They act as blueprints for creating objects that encapsulate both data (properties) and functionality (methods).

At Terra, classes are the foundation of our component architecture. We use them to build interactive UI components that can be initialized, managed, and destroyed cleanly across page transitions. Every class follows a consistent structure that ensures predictability, maintainability, and proper memory management.

Classes are also the backbone of our framework’s functionality.

Key Terra class principles:

  • Consistent structure - All classes follow the same constructor → init → events pattern
  • DOM organization - DOM elements are stored in a dedicated this.DOM object
  • Object parameters - All classes accept constructor’s parameters as a payload object for flexibility
  • Proper cleanup - Every class includes a destroy() method to prevent memory leaks
  • Clear documentation - All classes are documented with usage examples

This guide explains Terra’s class conventions and best practices. We’ll use a regular “library” style class as examples for all explanations. The other pattern we follow is the handler, you can find more about that here


The constructor is where you receive parameters and set up the initial state of your class.

All of our constructors will follow a common pattern:

  • If extending another class, we usually send the whole payload into the other class
class Handler extends CoreHandler {
constructor(payload) {
super(payload);
...
}
...
}
  • We create the functions and variables we’re going to use in the constructor, if they’re available at instantiation. If not, there’s no need, but you can add them to have the full blueprint in the constructor.
class Header {
constructor(payload) {
this.DOM = {
nav: payload.nav,
dropdowns: [...payload.dropdowns],
burger: payload.burger,
overlay: payload.overlay,
};
this.Manager = payload.Manager;
this.burger = null;
this.dropdownItems = [];
this.gsap = this.Manager.getLibrary("GSAP").gsap;
this.canPlay = true;
...
}
}
  • We (almost) always execute the class’ init and events method from our constructor, so the chain of operations starts when instantiating the class.
class Header {
constructor(payload) {
...
this.init();
this.events();
}
}

For classes that interact with the DOM, always include a this.DOM = {} object to store DOM elements.

The primary DOM element must be stored in this.DOM.element for consistency across all Terra classes. This element should use the js-- prefix to indicate it’s targeted by JavaScript.

class InfiniteMarquee{
constructor(payload){
var { element, speed, controlsOnHover, reversed, Manager } = payload;
this.DOM = { element };
...
}
...
}
// Usage in a handler
this.config = ({element}) => ({
Manager: this.Manager,
element:element,
speed: element.getAttribute("data-speed") || 1,
controlsOnHover:element.getAttribute("data-controls-on-hover") || "false",
reversed: element.getAttribute("data-reversed") || "false",
})

And the element would look like this:

<ul class="c--marquee-a js--marquee">
{Array.from({ length: 15 }).map((_, index) => (
<div class="c--marquee-a__item">{index + 1}</div>
))}
</ul>

Other example with direct instantiation:

class Header {
constructor(payload) {
this.DOM = {
nav: payload.nav,
dropdowns: [...payload.dropdowns],
burger: payload.burger,
overlay: payload.overlay,
};
}
...
}
// Usage
new Header({
nav: document.querySelector(".js--nav"),
dropdowns: document.querySelectorAll(".js--dropdown"),
burger: document.querySelector(".js--burger"),
overlay: document.querySelector(".js--overlay"),
Manager: this.Manager,
});

In any case, the main element of the DOM must be the element on which the class is being applied and must be only one element, not an array of them. If we have an array, we’ll loop through it and create a new instance for each one of them (which is what happens when we use our handlers).


The init() and events() methods organize your class functionality into two clear categories:

  • init() - Initialization logic, setting initial states, animations on load, configuring sliders, etc.
  • events() - EventListener setup for user interactions (clicks, scrolls, hovers, etc.)

Both methods are required.

class Header {
constructor(payload) {
this.DOM = {
nav: payload.nav,
dropdowns: [...payload.dropdowns],
burger: payload.burger,
overlay: payload.overlay,
};
...
this.init();
this.events();
}
/**
* Initializes header and all child components.
*/
init() {
if (this.DOM.burger && this.DOM.nav) {
...
}
if (this.DOM.dropdowns.length) {
this.DOM.dropdowns.forEach((dropdownEl) => {
this.dropdownItems.push(
new Dropdown({
dropdownEl: dropdownEl,
Manager: this.Manager,
getCanPlay: () => this.canPlay,
setCanPlay: (val) => (this.canPlay = val),
checkIfAnythingIsOpen: this.checkIfAnythingIsOpen,
updateOverlay: this.updateOverlay,
})
);
});
}
}
/**
* Sets up event listeners for resize and click outside detection.
*/
events() {
window.addEventListener("resize", debounce(this.closeOnResize, 10));
document.addEventListener("click", this.handleClickOutside);
}
}
export default Header;

Even if we don’t see an initial use for both our methods, we always include both of them in our classes for consistency. Do not omit any of the two methods when building new classes.

When logic starts to get a bit too much or will be reused across more than one event, use helper functions to store that logic and call it when needed. If a helper method is only going to be used inside the class, you can make it private adding a # to the beginning of its name.

If it’s possible that you’ll need to call that method from outside this class, leave it as public, as in the following example.

import {horizontalLoop} from '@andresclua/infinite-marquee-gsap';
import { u_stringToBoolean } from '@andresclua/jsutil';
class InfiniteMarquee{
constructor(payload){
var { element, speed, controlsOnHover, reversed, Manager } = payload;
this.DOM = { element };
this.gsap = Manager.getLibrary("GSAP").gsap;
this.speed = speed;
this.controlsOnHover = u_stringToBoolean(controlsOnHover);
// Define reversed attribute and initial direction
this.reversed = u_stringToBoolean(reversed);
this.initialDirection = this.reversed ? -1 : 1;
this.paused = false;
this.init();
this.events();
}
init(){
...
}
events(){
if (this.controlsOnHover && this.DOM.element?.parentElement){
this.mouseEnterHandler = () => this.pause();
this.mouseLeaveHandler = () => this.play();
const parent = this.DOM.element;
parent.addEventListener("mouseenter", this.mouseEnterHandler);
parent.addEventListener("mouseleave", this.mouseLeaveHandler);
}
}
pause(){
this.paused = true;
this.gsap.to(this.loop, { timeScale: 0, overwrite: true });
}
play(){
if (this.paused) {
// Always go back to the initial direction
this.gsap.to(this.loop, { timeScale: this.initialDirection, overwrite: true });
this.paused = false;
}
}
destroy(){
...
}
}
export default InfiniteMarquee;

Since we use SWUP for page transitions, it’s essential to include a destroy() method that removes event listeners and clears references. This prevents memory leaks when components are removed during transitions.

What to clean up in destroy():

  • Remove all event listeners
  • Clear handler references
  • Clear DOM references
  • Kill animations/timelines
  • Clear any intervals or timeouts
class InfiniteMarquee{
...
destroy(){
// Remove event listeners
if (this.controlsOnHover && this.DOM.element?.parentElement) {
const parent = this.DOM.element;
parent.removeEventListener("mouseenter", this.mouseEnterHandler);
parent.removeEventListener("mouseleave", this.mouseLeaveHandler);
// Clear handler references
this.mouseEnterHandler = null;
this.mouseLeaveHandler = null;
}
// Kill loop
if (this.loop) {
this.loop.kill();
this.loop = null;
}
// Clear all properties
this.gsap = null;
this.speed = null;
this.controlsOnHover = null;
this.reversed = null;
this.initialDirection = null;
this.paused = null;
this.DOM = null;
}
}

Why this matters:

With SWUP page transitions, content is replaced dynamically. Without proper cleanup:

  • Event listeners remain attached to removed elements
  • Memory usage increases over time
  • Multiple handlers can fire for the same action
  • Performance degrades

All classes must include clear documentation. Even if the class seems self-explanatory, documentation ensures that developers of all skill levels can understand and use the code.

Documentation should include:

  • Purpose of the class
  • Parameters with types and descriptions
  • Usage example
/**
* InfiniteMarquee - A GSAP-powered infinite horizontal marquee component
*
* Creates smooth, continuous scrolling animations for a list of elements.
* Supports hover controls, variable speed, and bi-directional scrolling.
*
* @class InfiniteMarquee
*
* @param {Object} payload - Configuration object
* @param {HTMLElement} payload.element - The container element whose children will be animated in the marquee
* @param {number} [payload.speed=1] - The speed of the marquee animation. Higher values = faster scrolling
* @param {string|boolean} [payload.controlsOnHover="false"] - When "true", pauses the marquee on mouse enter and resumes on mouse leave
* @param {string|boolean} [payload.reversed="false"] - When "true", the marquee scrolls in the opposite direction
* @param {Object} payload.Manager - The Manager instance that provides access to GSAP library via Manager.getLibrary("GSAP")
*
* @example
* new InfiniteMarquee({
* element: document.querySelector('.js--Marquee'),
* speed: 2,
* reversed: true,
* controlsOnHover: true
* });
*/
class InfiniteMarquee {
...
}

Here’s the full InfiniteMarquee class we’ve been dissecting:

import { horizontalLoop } from "@andresclua/infinite-marquee-gsap";
import { u_stringToBoolean } from "@andresclua/jsutil";
/**
* InfiniteMarquee - A GSAP-powered infinite horizontal marquee component
*
* Creates smooth, continuous scrolling animations for a list of elements.
* Supports hover controls, variable speed, and bi-directional scrolling.
*
* @class InfiniteMarquee
*
* @param {Object} payload - Configuration object
* @param {HTMLElement} payload.element - The container element whose children will be animated in the marquee
* @param {number} [payload.speed=1] - The speed of the marquee animation. Higher values = faster scrolling
* @param {string|boolean} [payload.controlsOnHover="false"] - When "true", pauses the marquee on mouse enter and resumes on mouse leave
* @param {string|boolean} [payload.reversed="false"] - When "true", the marquee scrolls in the opposite direction
* @param {Object} payload.Manager - The Manager instance that provides access to GSAP library via Manager.getLibrary("GSAP")
*
* @example
* new InfiniteMarquee({
* element: document.querySelector('.js--Marquee'),
* speed: 2,
* reversed: true,
* controlsOnHover: true
* });
*/
class InfiniteMarquee {
constructor(payload) {
var { element, speed, controlsOnHover, reversed, Manager } = payload;
this.DOM = { element };
this.gsap = Manager.getLibrary("GSAP").gsap;
this.speed = speed;
this.controlsOnHover = u_stringToBoolean(controlsOnHover);
// Define reversed attribute and initial direction
this.reversed = u_stringToBoolean(reversed);
this.initialDirection = this.reversed ? -1 : 1;
this.paused = false;
this.init();
this.events();
}
init() {
this.loop = horizontalLoop(this.DOM.element.children, {
paused: false,
repeat: -1,
reversed: this.reversed,
speed: this.speed,
});
this.gsap.set(this.loop, { timeScale: this.initialDirection });
}
events() {
if (this.controlsOnHover && this.DOM.element?.parentElement) {
this.mouseEnterHandler = () => this.pause();
this.mouseLeaveHandler = () => this.play();
const parent = this.DOM.element;
parent.addEventListener("mouseenter", this.mouseEnterHandler);
parent.addEventListener("mouseleave", this.mouseLeaveHandler);
}
}
pause() {
this.paused = true;
this.gsap.to(this.loop, { timeScale: 0, overwrite: true });
}
play() {
if (this.paused) {
// Always go back to the initial direction
this.gsap.to(this.loop, { timeScale: this.initialDirection, overwrite: true });
this.paused = false;
}
}
destroy() {
// Remove event listeners
if (this.controlsOnHover && this.DOM.element?.parentElement) {
const parent = this.DOM.element;
parent.removeEventListener("mouseenter", this.mouseEnterHandler);
parent.removeEventListener("mouseleave", this.mouseLeaveHandler);
// Clear handler references
this.mouseEnterHandler = null;
this.mouseLeaveHandler = null;
}
// Kill loop
if (this.loop) {
this.loop.kill();
this.loop = null;
}
// Clear all properties
this.gsap = null;
this.speed = null;
this.controlsOnHover = null;
this.reversed = null;
this.initialDirection = null;
this.paused = null;
this.DOM = null;
}
}
export default InfiniteMarquee;

This example demonstrates:

  • ✅ Complete documentation with parameters and usage
  • ✅ Proper this.DOM structure
  • ✅ Clear init() and events() separation
  • ✅ Conditional event listener setup
  • ✅ Additional methods for specific functionality
  • ✅ Thorough destroy() method
  • ✅ Default export for easy importing

Terra’s class structure ensures:

  • Consistency across all projects
  • Maintainability through clear organization
  • Memory safety with proper cleanup
  • Scalability with multiple instances
  • Documentation for all team members

Knowledge Check

Test your understanding of this section

Loading questions...