Contact Us

Vanilla JS Interactivity Patterns

DOC-00110 guide implementor, developer

Overview

The core theme ships no JavaScript framework dependency. All interactive behavior uses self-contained vanilla JavaScript following Starwind-pattern conventions. Each component owns its DOM discovery, state management, event handling, and lifecycle cleanup.

Core Conventions

Component Part Identification — data-slot

Components use data-slot attributes to identify meaningful DOM parts. Scripts discover their elements via data-slot selectors rather than relying on class names or DOM structure.

<div data-slot="accordion">
  <button data-slot="accordion-trigger">...</button>
  <div data-slot="accordion-panel">...</div>
</div>

State Management — data-state

Interactive state is tracked via data-state attributes. CSS transitions and visibility rules target these selectors. The base CSS rule [data-slot][data-state="closed"] { display: none !important } provides FOUC prevention for elements that should be hidden on initial render (modals, drawers, tooltips). The !important is intentional: it ensures the closed state still wins when the element also carries utility classes such as flex, grid, or block.

Elements that use a different visibility mechanism (such as CSS grid-template-rows for accordion collapse) should avoid combining data-slot and data-state="closed" on the same element to prevent the base rule from interfering.

Instance Storage — WeakMap

Component handler instances are stored in a WeakMap<HTMLElement, Handler>. This is GC-friendly — when the DOM element is removed, the handler instance becomes eligible for garbage collection without manual cleanup.

const instances = new WeakMap<HTMLElement, MyHandler>();

function setup() {
  document
    .querySelectorAll<HTMLElement>('[data-slot="my-component"]')
    .forEach((el) => {
      if (instances.has(el)) return; // prevent double-init
      instances.set(el, new MyHandler(el));
    });
}

TypeScript Classes for Medium/Complex Components

Medium and complex components use TypeScript classes that encapsulate state, event handling, and DOM manipulation. Simple behaviors (e.g., a toggle button) may use plain functions.

Lifecycle Hooks — View Transitions Compatibility

Each component initializes on both initial page load and SPA navigation:

  • setup() runs at module scope for the initial load
  • document.addEventListener("astro:page-load", setup) handles SPA re-navigation
  • The WeakMap.has(el) guard prevents double-initialization on persisted elements

Event Listener Cleanup — AbortController

Window/document event listeners registered per component instance use AbortController for cleanup. Each instance creates an AbortController on init and passes { signal } to addEventListener. On astro:before-swap, the controller is aborted, removing all associated listeners in one call.

class MyHandler {
  private ac = new AbortController();

  constructor(el: HTMLElement) {
    window.addEventListener("scroll", this.onScroll, {
      passive: true,
      signal: this.ac.signal,
    });
  }

  destroy() {
    this.ac.abort();
  }
}

// Cleanup before view transition swap
document.addEventListener("astro:before-swap", cleanup);

Singleton listeners (e.g., global keyboard shortcuts) register once at module scope with a guard, not per-instance.

Progressive Enhancement

All core content and navigation links are accessible without JavaScript. Interactive enhancements require JS. Components render in a sensible default state without JS — accordion items expanded, navigation links visible, modals hidden via data-state="closed".

Modal overlays (search modal, lightbox, drawer) use the native HTML <dialog> element with .showModal(), which provides built-in focus trapping, scroll lock, Escape key dismissal, and ::backdrop styling. Exit animations use JS-managed timing — set data-state="closed", wait for the CSS transition duration, then call .close().

Collapse Animations — CSS grid-template-rows

Expandable content (accordions, collapsible sections) uses CSS grid-template-rows: 0fr / 1fr transitions. The JS responsibility is limited to toggling the state attribute; the animation is purely CSS.

REQ-00010 normative Client-side JavaScript usage shall be minimized while preserving functional richness.
REQ-00046 normative Framework-free components are preferred.
REQ-00260 implemented Cross-component communication shall use named custom DOM events dispatched on the window object (e.g., theme-changed, search:open, listbox-change) rather than direct component coupling. Event names and payload shapes shall be treated as public API contracts.

Search

Search across pages and articles. Use arrow keys to navigate results.

Search across pages and articles.

Loading search...

Search is unavailable. Please try again later.

    No results for ""

    Try different keywords or fewer words.