JavaScript
All JavaScript on this site is vanilla — no frameworks, no external
libraries. Every interaction lives in a <script> block before </body>
on the page it belongs to. There are no shared JS files.
Homepage interactions
The homepage (index.html) contains all of the site's JavaScript. The
interactions are listed below in the order they appear in the script block.
Particle network
Renders an animated gold particle field on the hero section using the HTML5 Canvas API.
| Property | Value |
|---|---|
| Element | <canvas id="heroCanvas"> |
| Particle count | Calculated from canvas area — maximum 80 |
| Particle colour | rgba(184,150,46, variable opacity) |
| Connection distance | 120px — particles within range are connected by a line |
| Mouse interaction | Particles repel from the cursor within a 150px radius |
| Performance | Pauses via IntersectionObserver when the hero is out of view |
Cursor trail
Renders small gold particles that follow the cursor across the page.
| Property | Value |
|---|---|
| Element | Particles are appended directly to document.body |
| z-index | 9997 — below the command palette overlay (9998) and noise overlay (9998) |
| Particle size | 4px circle |
| Particle colour | rgba(184,150,46, 0.6) |
| Maximum particles | 15 active at once |
| Throttle | One particle every 30ms |
| Pointer events | none — particles never block clicks |
Scrolling gold strip
The strip below the hero is a pure CSS animation — there is no JavaScript involved. It is documented here to prevent a maintainer from looking for JavaScript that does not exist.
The animation works by duplicating the strip-track element in HTML so
the second copy begins exactly where the first ends. The CSS stripScroll
keyframe moves both copies left by 50% simultaneously, creating a seamless
loop. See styles.css for the .strip-track and @keyframes stripScroll
rules.
Scroll progress bar
A gold bar at the top of the viewport that tracks scroll depth.
| Property | Value |
|---|---|
| Element | <div id="progressBar"> |
| Trigger | window scroll event, passive listener |
| Calculation | scrollTop / (scrollHeight - clientHeight) * 100 |
Nav scroll state
The nav gains a scrolled class when the user scrolls more than 40px
from the top. This triggers a background and shadow change defined in
styles.css.
| Property | Value |
|---|---|
| Element | <nav id="nav"> |
| Threshold | 40px from top |
| Class added | scrolled |
Ambient orb animation
Three blurred circular gradients behind the social proof and CTA sections animate on a continuous sine-wave path.
| Property | Value |
|---|---|
| Elements | .ambient-orb |
| Animation | Math.sin / Math.cos path, updates every frame via requestAnimationFrame |
| Performance | Runs continuously — not paused when out of view. Acceptable at current orb count (three per section). If orb count increases significantly, add IntersectionObserver pause logic matching the pattern used in the particle network. |
Scroll reveal
Elements with the class reveal fade up into view as they enter the
viewport.
| Property | Value |
|---|---|
| Trigger class | reveal |
| Active class | visible |
| Mechanism | IntersectionObserver, threshold 0.1, rootMargin -40px |
| Delay variants | reveal-d1, reveal-d2, reveal-d3 — CSS animation delays |
| Unobserves | Yes — each element is unobserved after it becomes visible |
Animated counters
Stat numbers count up from zero when they enter the viewport. Triggered
by the scroll reveal IntersectionObserver.
| Property | Value |
|---|---|
| Trigger attribute | data-count — target number |
| Suffix attribute | data-suffix — appended after the number (e.g. +) |
| Duration | 900ms |
| Easing | Cubic ease-out |
| Runs once | Yes — data-animated flag prevents re-triggering |
Typewriter effect
Cycles through three documentation snippets in the hero code panel.
| Property | Value |
|---|---|
| Element | <span id="twCode"> |
| Snippets | Three — docs strategy, audit dimensions, deliverables |
| Typing speed | 45ms per character |
| Delete speed | 20ms per character |
| Pause at end | 2800ms before deleting |
| Pause between | 400ms before typing the next snippet |
Social proof carousel
Rotates three items in the hero side panel automatically.
| Property | Value |
|---|---|
| Elements | .side-carousel-item |
| Interval | 4000ms |
| Active class | active |
| Controls | Dot buttons — clicking a dot pauses the timer and jumps to that item |
Documentation health check widget
The most complex component on the homepage. A six-question scoring engine that produces a personalised health report.
States:
| State | Description |
|---|---|
| Questions | Six questions displayed one at a time with a step indicator |
| Results | Score ring, category label, three dimension health bars, per-answer findings, weakest dimension summary |
| Reset | All state clears and returns to question one |
Scoring:
| Dimension | Questions |
|---|---|
| Accuracy | Q1, Q5 |
| Ownership | Q2, Q6 |
| Findability | Q3, Q4 |
Each answer carries a score of 0, 1, 2, 3, or 5. The total is expressed as a percentage of the maximum possible score. Dimension scores are averaged independently and displayed as health bars.
Score categories:
| Range | Label |
|---|---|
| 80–100% | Strong foundation |
| 60–79% | Room to improve |
| 40–59% | At risk |
| 0–39% | Needs urgent attention |
Command palette
Opens with ⌘K on Mac or Ctrl+K on Windows. Searches portfolio
pieces and navigation destinations.
| Property | Value |
|---|---|
| Trigger | ⌘K / Ctrl+K or the search button in the nav |
| Close | Escape or clicking outside the panel |
| Items | Six — audit booking, health check, four portfolio pieces, email |
| Filter | Live text match on label and description |
Hamburger navigation
Controls the mobile nav menu on all pages.
| Property | Value |
|---|---|
| Trigger element | <button id="navHamburger"> |
| Toggle class | open on #navLinks and #navHamburger |
| Auto-close | On nav link click (50ms delay) and on outside click |
| Accessibility | aria-expanded toggled on the button |
Smooth scroll
Intercepts all anchor clicks (href starting with #) and replaces
default browser scroll with a custom eased animation.
| Property | Value |
|---|---|
| Selector | document.querySelectorAll('a[href^="#"]') |
| Duration | 800ms |
| Easing | Quartic ease-out — 1 - Math.pow(1 - progress, 4) |
Writing samples tab switcher
Controls the four tabbed panels in the writing samples section.
| Property | Value |
|---|---|
| Trigger | switchTab(btn, id) called via onclick on each tab button |
| Active class | active — toggled on both the selected button and its panel |
| Transition | Controlled by CSS on .sample-panel — panels without active are hidden |
| Default active | First tab (panel-api) has active class set in HTML on page load |
| Current panels | panel-api, panel-start, panel-arch, panel-git |
To add a new tab:
-
Add a button in the
.samples-tabsdiv: -
Add the corresponding panel div after the last
.sample-panel: -
Do not add
activeto the new button or panel — the default active state is set in HTML on the first tab only. The switcher handles everything else. -
Add a row to the panels table above with the new panel ID.
Adding a new interaction
- Write the interaction as a self-contained function in the
<script>block before</body>inindex.html - Do not import external libraries — keep all interactions in vanilla JS
- Use
IntersectionObserverfor any animation that should pause when out of view - Add a row to the relevant table in this document describing the new interaction