assets/js/ — Client-Side JavaScript
All JavaScript is vanilla ES5/ES6 — no framework, no build step. Scripts are loaded at the bottom of _layouts/default.html (or the specific layout that needs them). Each module is self-contained and initializes on DOMContentLoaded or via an IIFE.
wavesurfer.min.js is the only third-party library and is vendored directly (no CDN dependency).
Module Reference
navigation.js
Loaded on: every page (via default.html)
Handles all header interaction:
- Mobile menu toggle — shows/hides
#mobile-menu, swaps hamburger/close icons, setsaria-expanded - Close on link click — collapses mobile menu when any nav link is tapped
- Sticky header shrink — adds
.is-scrolledto#site-headerafter 100px scroll (CSS drives the visual shrink) - Smooth scroll — intercepts anchor
hrefclicks and scrolls smoothly to the target element
category-filter.js
Loaded on: blog index page (pages/blog.html)
Client-side filtering of the post list by category — no page reload required.
- Reads category from
?cat=<slug>query parameter on page load to restore a shared/bookmarked filter - Clicking a pill updates the URL via
history.replaceState()(shareable links) - Shows an empty-state message when a filter returns zero results
- The “All” pill clears the active filter
How it works: each .post-list-item has a data-categories="slug1 slug2" attribute set in the template. The script shows/hides items by checking whether the active slug appears in that space-separated list.
scroll-animations.js
Loaded on: every page (via default.html)
Uses IntersectionObserver to add the .animated class to section elements as they enter the viewport. CSS handles the actual fade-in transition via .animate-on-scroll and .animated.
- Respects
prefers-reduced-motion— exits immediately if set, so no animation runs - Sections animate as single units (no stagger within a section)
- Observer disconnects from each element after it animates (
unobserve)
hero-slideshow.js
Loaded on: homepage (if slideshow images are present)
Cycles through hero photo images with crossfade transitions. Configuration comes from data-interval and data-transition attributes on .hero-slideshow, which are set from _data/site.yml via Liquid.
- One image has class
.activeat a time; the next receives it on each tick - Falls back gracefully if only one image exists (no timer started)
To change timing: edit bio.hero_slideshow_interval and bio.hero_slideshow_transition in _data/site.yml.
featured-carousel.js
Loaded on: homepage (if a [data-featured-carousel] element exists)
Infinite-loop carousel for the featured work section.
- Clones items at both ends of the track so wrapping appears seamless
- Number of visible items is responsive: 1 on mobile, 2 at ≥560px, 3 at ≥900px
- Prev/Next arrow buttons are hidden when total items ≤ visible count
- Handles window resize (recalculates visible count and repositions track)
card-tilt.js
Loaded on: every page with .card elements
Adds a cursor-reactive 3D tilt and spotlight glare overlay to all .card elements.
mousemove→ sets--card-rotate-x,--card-rotate-y,--mouse-x,--mouse-yCSS custom properties; adds.is-tiltingmouseleave→ resets all properties; removes.is-tilting(CSS spring-back transition activates)- Maximum tilt: 8 degrees
- Respects
prefers-reduced-motion— no handlers attached if set
portfolio-expand.js
Loaded on: homepage and /portfolio/ page
Collapses portfolio grids to two visible rows and adds a “See more” / “See less” toggle button. Row boundaries are calculated from offsetTop values, so it works with any grid column count.
- Hidden items get
visibility: hidden(notdisplay: none) to preserve grid layout - Button is not injected if all items fit within two rows
shuffle-grids.js
Loaded on: homepage (or any page with .auto-grid)
Randomly shuffles cards within each .auto-grid on page load. Featured cards (.card--featured) are always kept first; the rest are shuffled.
pill-marquee.js
Loaded on: project/performance cards with .card-pills-track
Detects when a pill row overflows its container and adds .is-overflowing to the track. CSS then activates a horizontal scroll marquee animation on hover. Re-runs on window resize.
gallery.js
Loaded on: project and performance detail pages
Two behaviors:
- Shuffle — randomizes the order of gallery images on page load
- Lightbox — clicking a gallery image opens a full-screen overlay with prev/next navigation and keyboard support (arrow keys, Escape to close)
performance-gallery.js
Loaded on: pages with .performance-card-image--gallery elements
For performance cards that have multiple images:
- On page load, picks a random image from
data-images(JSON array) and renders it as the card face - On click (or Enter/Space), opens a full-screen lightbox the user can cycle through
- Shows a loading spinner while images load; nav buttons are disabled during load with an automatic timeout fallback
vo-player.js
Loaded on: pages with VO clip cards
Lazy-initializes WaveSurfer.js v6 for voice-over clip playback.
- WaveSurfer instance is created only on first user interaction with a card (not on page load)
- Only one clip may play at a time — starting a new clip auto-pauses any other active instance
- Reads
--color-voice-overCSS custom property for waveform color wavesurfer.min.jsis vendored in this directory
wavesurfer.min.js
Third-party library (WaveSurfer.js v6), vendored to avoid a CDN dependency. Do not edit this file. To upgrade, replace with the new minified build from the WaveSurfer.js releases.
Loading a Script
Scripts are referenced at the bottom of the relevant layout file. To add a new script to every page, add it to _layouts/default.html. For page-specific scripts, add the <script> tag to the specific layout (e.g. _layouts/project.html).
<script src="/assets/js/my-module.js" defer></script>
Use defer unless the script must run before the DOM is parsed.