JavaScript Guidelines
Architecture overview
The design system uses a two-layer JavaScript architecture:
Layer 1 — Shared bundle (compiled once by Vite)
src/handoff/js/main.js
├── js/components/accordion.js — jQuery slide accordion
├── js/components/default-slider.js — Slick: .js-default-slider variants
├── js/components/article-card-slider.js— Slick: article card carousel
├── js/components/header.js — sticky header, notification banner
├── js/components/main-nav.js — primary navigation
├── js/components/mobile-nav.js — mobile nav drawer
├── js/components/hamburger-icon.js — hamburger toggle
├── js/components/search-bar.js — site search
├── js/components/offcanvas.js — Bootstrap offcanvas panel
├── js/components/jump-nav.js — in-page anchor navigation
├── js/components/tabs.js — tab switching (.js-toggle-tab)
├── js/components/selects.js — Select2 dropdowns
├── js/components/popup.js — Magnific Popup lightbox
├── js/components/reveal.js — ScrollReveal entrance animations
├── js/components/parallax.js — parallax scrolling effects
├── js/components/observer.js — IntersectionObserver utility
├── js/components/smooth-scroll.js — smooth anchor scrolling
├── js/components/comparison-slider.js — before/after comparison slider
├── js/components/compare-slider.js — compare slider variant
├── js/components/carousel-content.js — content carousel
├── js/components/cta-columns-slider.js— CTA columns slider
├── js/components/showcase-slider.js — product showcase slider
├── js/components/ticker-slider.js — ticker/marquee slider
├── js/components/video-carousel.js — video carousel
├── js/components/featured-content.js — featured content
├── js/components/speaker-card.js — speaker card expand/collapse
├── js/components/intro-label.js — intro label animation
├── js/components/gravity-forms.js — Gravity Forms enhancements
├── js/components/consent.js — cookie consent
├── js/components/pointer-events.js — pointer events utility
└── js/utilities/svg-inline-loader.js — inline SVG injection
Layer 2 — Per-block scripts (compiled per-block by Handoff)
src/handoff/blocks/{name}/script.js
— imports one or more Layer 1 utilities
— may add block-specific custom logic
— Vite deduplicates: each utility appears only once in the final bundleThe shared bundle is built once and deployed as public/api/component/main.js. Each block's script.js imports the utilities it needs. Vite deduplicates these at build time — no utility is included twice in the final output, regardless of how many blocks import it.
Dependencies
| Library | Version | Purpose |
|---|---|---|
| jQuery | 3.7.1 | DOM utilities, event handling, plugin compatibility |
| Bootstrap | 5.3.5 | Collapse, modal, dropdown, offcanvas plugins |
| Slick Carousel | 1.8.1 | Carousels and sliders |
| Magnific Popup | 1.2.0 | Lightbox, video popups, inline modals |
| Select2 | 4.1.0-rc.0 | Enhanced select dropdowns with search |
| ScrollReveal | 4.0.9 | Scroll-triggered entrance animations |
| axe-core | 4.11.0 | Automated accessibility testing |
| url-search-params-polyfill | 8.2.5 | URLSearchParams API polyfill |
Build tooling
JavaScript is bundled with Vite via the handoff.config.js build pipeline:
1// src/handoff/handoff.config.js
2entries: {
3 js: './js/main.js',
4 bundle: './js/main.js',
5}Build commands:
1npm run start # development server with live reload
2npm run build:components # build component bundle
3npm run build:app # build the documentation siteShared utilities
Each file in js/components/ handles one concern and is auto-initialized on DOMContentLoaded. Blocks import the utilities they need in their script.js.
| File | Trigger | Used by blocks |
|---|---|---|
accordion.js | .c-accordion__toggle | accordion-simple, callout, content, feature |
default-slider.js | .js-default-slider, .js-default-slider-alt | banner, testimonial-slider |
article-card-slider.js | .js-article-card-slider | posts-featured |
header.js | .c-header__bottom (sticky), .c-notification__close | header |
main-nav.js | .c-main-nav | header |
mobile-nav.js | .c-mobile-nav | header |
hamburger-icon.js | .c-hamburger | header |
search-bar.js | .c-site-search | header |
offcanvas.js | .c-offcanvas | header |
jump-nav.js | .c-jump-nav | header, hero-product |
tabs.js | .js-toggle-tab, .c-tabs | any block with tabs |
selects.js | .c-select select, .event-form-select, .c-provider-finder__select | cta-provider, download, downloads-*, footer, grid-three-column, hero-contact, hero-news, hero-search-provider, posts-latest |
popup.js | .js-open-content-popup, .js-open-video, .eb-js-showcase-slider | download, downloads-*, hero-contact |
reveal.js | .will-slide-right, .will-slide-left, .will-fade-in | global (any block) |
parallax.js | [data-parallax] | global (any block) |
smooth-scroll.js | .js-smooth-scroll | global (any block) |
speaker-card.js | .c-speaker-card | content-bio-list |
observer.js | IntersectionObserver setup | global utility |
gravity-forms.js | .gform_wrapper | any block with a Gravity Form |
consent.js | .c-consent | notification banner |
svg-inline-loader.js | inline <use> SVG references | most hero and event blocks |
The @shared annotation
When a script.js delegates entirely to a shared utility, document it with a JSDoc @shared tag instead of leaving a bare import. This lets Handoff's component preview surface meaningful context.
1/**
2 * Accordion Simple Block
3 *
4 * Expandable content panels using jQuery slide animations.
5 *
6 * @shared accordion
7 * Trigger: .c-accordion__toggle (scoped to .c-accordion blocks on this page)
8 * Behavior: Expands one panel at a time; collapses siblings
9 *
10 * @see /guidelines/javascript/shared-utilities
11 */
12import '../../js/components/accordion.js';See Component Scripts for the full annotation pattern.
Multi-instance pattern
When writing custom block-level JavaScript, always support multiple instances of the same block on one page. Scope all DOM queries to the block element:
1document.addEventListener('DOMContentLoaded', () => {
2 document.querySelectorAll('[data-component="my-block"]').forEach(block => {
3 const trigger = block.querySelector('.js-trigger');
4 const panel = block.querySelector('.js-panel');
5
6 if (!trigger || !panel) return;
7
8 trigger.addEventListener('click', () => {
9 const isOpen = panel.classList.contains('is-open');
10 panel.classList.toggle('is-open', !isOpen);
11 trigger.setAttribute('aria-expanded', String(!isOpen));
12 });
13 });
14});Never query document.getElementById or scope queries to document when you need per-instance behavior — always use block.querySelector.
Coding style
- Use
constandlet— nevervarin new code - Prefer vanilla
document.querySelectorover jQuery$()in new scripts - Wrap initialization in
DOMContentLoadedor iterate with.forEach(block => ...) - Remove all
console.logbefore committing - Use
class="js-*"selectors for JavaScript hooks — notid="*" - Always include a JSDoc header on each
script.jswith@sharedor@requires, and@seelinks
Sub-pages
- Component Scripts — how to write and annotate
script.jsfiles - Shared Utilities — full reference for every module in
js/components/ - Slick Carousel — slider configuration and initialization
- Select2 — select dropdown patterns
- Magnific Popup — lightbox and video popup patterns
On This Page