Scripting
How Zazz scripts are structured, documented, and initialized: the vanilla-JavaScript conventions behind every browser-native behavior.
Scripts in zazz/scripts/ are served directly at /zazz/scripts/*.js, with no bundler or transpiler. They are native ES modules and run as-is in modern browsers. A single entry module, main.js, imports every component script, so a page loads behavior with one <script type="module" src="./zazz/scripts/main.js"> tag (see Structuring your head tag).
Philosophy
- Vanilla JS only. No framework, no npm runtime deps, no build step — just native ES modules. Cross-script dependencies use
import; external libraries Zazz doesn't ship (the Embla CDN UMD bundles) are still read as globals loaded by prior<script>tags. - HTML-first. Markup and data attributes drive behavior. Authors configure components in HTML; scripts discover and enhance the DOM.
- Progressive enhancement. Feature-detect APIs before use. When unsupported, degrade gracefully.
- Minimal surface area. Export a small public API. Keep helpers private.
File structure
Every script follows this layout:
"use strict";
/**
* @fileoverview Module title.
* @description What this module does.
*/
import { Dependency } from "./dependency.js"; // only when this module needs another
// Implementation
// Auto-initialize when DOM is ready (only in browser environment)
if (typeof window !== "undefined" && typeof document !== "undefined") {
// ...
}
// Attach to window for the documented public API, then export for module consumers
// (the main.js entry and any sibling script that imports this one).
if (typeof window !== "undefined") {
window.MyExport = MyExport;
}
export { MyExport };Module exports
Each script attaches its public API to window (the documented surface) and provides a matching named export that the main.js bundle and sibling modules import:
| File | Global | Export shape |
|---|---|---|
utils.js | window.Utils | { parseValue, parseDataAttributes } |
reveal.js | window.Reveal | Reveal class |
embla.js | window.EmblaInit | { init, initRoot, addDotBtnsAndClickHandlers } |
navigation.js | (none) | Side-effect only; no export |
The web-component scripts (carousel.js, lightbox.js, password.js, tabs.js) export their element class and register the custom element as a side effect.
Auto-initialization
Scripts that enhance the page on load use a guarded auto-init block:
if (typeof window !== "undefined" && typeof document !== "undefined") {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
}Initialization is idempotent: guarded with attributes (data-embla-init), flags, or instance checks so re-running is safe.
Data-attribute configuration
Component scripts read configuration from HTML data attributes rather than JS options objects:
- Use a consistent prefix per component:
data-embla-*,data-reveal-*. - Parse attributes with
Utils.parseDataAttributes(node, "data-embla-"). - Boolean flags can be bare attributes (
data-embla-autoplay) or explicit values (data-embla-keyboard="false").
Dependencies and load order
Cross-script dependencies are real ES imports, so the module graph resolves order — you load main.js and nothing else needs sequencing:
| Script | Imports | Notes |
|---|---|---|
utils.js | — | Provides window.Utils |
reveal.js | — | Standalone |
embla.js | utils.js | Also needs the Embla CDN globals (see below) |
carousel.js | embla.js | <embla-carousel> calls EmblaInit.initRoot |
lightbox.js | carousel.js | <media-lightbox> coordinates carousel elements |
password.js | — | Standalone (<input-password>) |
tabs.js | — | Standalone (<tab-group>) |
navigation.js | — | App-level; inert in component preview iframes |
The one ordering the module graph can't enforce is the Embla CDN UMD bundles: embla.js reads them as globals, so their <script defer> tags must precede the main.js module in the document. Where these tags go is covered in Structuring your head tag.
DOM interaction patterns
- Query within scope. Accept an optional root element so init can target a subtree.
- Early returns for guards. Check required elements and skip gracefully.
- Store instances on DOM nodes when external access is needed:
emblaNode._emblaApi = emblaApi. - Observe DOM changes with
MutationObserverfor elements hidden at init time (closed dialogs). - Delegate events at
documentlevel when triggers can appear anywhere. - Respect focus and input context. Skip keyboard handlers in form fields or contenteditable.
Classes vs functions
- Use a class when the module manages persistent instance state (
Revealwith observers and config). - Use functions for stateless init and helpers (
initEmblaCarousels,parseValue). - Use private class fields (
#observers,#getObserver) for encapsulation.
JSDoc conventions
Every JSDoc block must include @description. Tag order:
@description
@param
@returns
@private
@see
@exampleRequired tags
| Tag | Required on |
|---|---|
@fileoverview | Every file |
@description | Every JSDoc block |
@param | Functions with parameters |
@returns | Functions that return a value |
@private | Non-exported helpers |
@typedef | Config/option objects |
@namespace | Export objects (Utils, EmblaInit) |
Section dividers
Use thin single-line markers to group related code:
// --- Dot navigation ---Do not use banner block comments (/* ==== ... ==== */).
Syntax and style
- Double quotes for strings.
- Semicolons required.
- Optional chaining (
?.) and nullish coalescing where they simplify guards. - No TypeScript source files; types live in JSDoc and
globals.d.ts. - Type-checking enabled via
tsconfig.json(checkJs: true).