Component System

JS components are auto-loaded from src/js/components/ based on data-component attributes in HTML. All components extend a base Component class.

HTML attributes

Attach a component to any element with data-component. The value must match the filename in src/js/components/.

<!-- Basic component -->
<div data-component="MyComponent">...</div>

<!-- With options -->
<div data-component="Slider" data-options='{"speed": 500}'>...</div>

<!-- Responsive: only init on medium+ -->
<div data-component="Sidebar" data-media="mediumUp">...</div>

<!-- Persistent: survives Swup page transitions -->
<nav data-component="NavList" data-persistent="true">...</nav>

Refs

Use data-ref to reference child elements. Single elements become a direct reference, multiple same-name refs become an array.

<div data-component="Tabs">
  <button data-ref="trigger">Tab 1</button>
  <button data-ref="trigger">Tab 2</button>
  <div data-ref="content">Panel 1</div>
  <div data-ref="content">Panel 2</div>
</div>

// In JS:
// this.ref.trigger → [button, button]
// this.ref.content → [div, div]

Creating a component

Create a file in src/js/components/ matching the data-component name.

// src/js/components/Accordion.js
import Component from '../core/Component'

export default class Accordion extends Component {
  constructor(element, options = {}) {
    super(element, options)
  }

  prepare() {
    // Called after construction
    // this.element — the root DOM element
    // this.ref     — data-ref elements
    // this.options — parsed from data-options

    this.ref.trigger.forEach(btn => {
      btn.addEventListener('click', this.toggle)
    })
  }

  toggle(e) {
    // Methods are auto-bound to `this`
    const index = this.ref.trigger.indexOf(e.currentTarget)
    this.ref.content[index].classList.toggle('is-open')
  }

  destroy() {
    // Cleanup: remove listeners, observers, etc.
    this.ref.trigger.forEach(btn => {
      btn.removeEventListener('click', this.toggle)
    })
  }
}

Lifecycle

Every component goes through three phases managed by src/js/core/componentUtils.js.

  1. constructor — refs are auto-initialized, methods auto-bound
  2. prepare() — add event listeners, set up observers, fetch data
  3. destroy() — clean up on page transition or viewport mismatch

How it works

1. Page loads (or Swup transition fires)
2. componentUtils scans for [data-component] elements
3. Each component JS file is dynamically imported
4. constructor() runs → refs parsed, methods bound
5. prepare() runs → your init code
6. On page leave: destroy() runs for non-persistent components

Responsive components

Components with data-media are automatically destroyed when the viewport no longer matches and re-initialized when it does. A resize listener handles this.

Available breakpoint keys

Defined in src/js/core/config.js, mirroring the Stylus breakpoints:

smallUp, smallMax, smallWideUp, smallWideMax,
mediumUp, mediumMax, largeUp, largeMax,
xlargeUp, xlargeMax, xxlargeUp, xxlargeMax,
xxxlargeUp

Example

<!-- Sidebar only initializes at 768px+ -->
<aside data-component="Sidebar" data-media="mediumUp">
  ...
</aside>

<!-- Tooltip disabled on mobile -->
<div data-component="Tooltip" data-media="largeUp">
  ...
</div>

Persistence

By default, components are destroyed and re-initialized on every Swup page transition. Mark a component as data-persistent="true" to keep it alive across navigations.

<!-- NavList stays alive across page transitions -->
<nav data-component="NavList" data-persistent="true">
  ...
</nav>

Persistent components must live outside the #swup container (or in a shared partial like header.html) so they remain in the DOM after transitions.

File structure

src/js/
  app.js                    // Entry — Swup + Lenis setup
  core/
    Component.js            // Base class (refs, lifecycle, auto-bind)
    componentUtils.js       // Dynamic loading, responsive init, persistence
    config.js               // Breakpoints mirrored from Stylus
  components/
    NavList.js              // Navigation with active state
    LucideGrid.js           // Lucide icon loader
    YourComponent.js        // Add new components here
  utils/
    dom.js                  // DOM helpers (query, queryAll)

Quick start

  1. Create src/js/components/MyComponent.js
  2. Add data-component="MyComponent" to your HTML element
  3. Implement prepare() and destroy()
  4. It auto-loads — no imports or registration needed