Skip to main content
James Music

Hi! I'm James. I'm blind.
I build things that work for everyone.

Software Engineer. Sound Engineer. Musician. Accessibility Expert.

25 years turning complex problems into production-grade solutions.

About Mr. Music

Born blind, I learned to think in systems before I could see them. That turned out to be a superpower.

I've spent 25 years as a software engineer, shipping everything from startup MVPs to enterprise platforms. I'm also a trained sound engineer, music producer, and multi-instrumentalist with over 30 years playing by ear.

I consult on accessibility because I live it — screen reader, braille display, the lot. When I build or advise, it works for everyone.

Book a 15-minute call

Pick a time that works for you. I'll send a Google Meet link.

Pick a day

Mon
Tue
Wed
Thu
Fri

June 2026

Notes from James

What I'm building, thinking about, and working on.

Check out in .

Building

The theme hook watches a DOM attribute instead of wrapping the app in a provider

The site has a dark mode and a light mode. Every component that changes colour between themes needs to know which theme is active. In React, the standard approach is a Context provider — wrap the tree, consume the value, re-render on change. The useTheme hook takes a different path. It reads the data-theme attribute from the document element and subscribes to changes through a MutationObserver. The observer watches one attribute on one element. When the ThemeToggle component sets data-theme to light or dark, the observer callback fires, reads the new value, and updates the hook's state. Every component that calls useTheme gets its own independent subscription to the DOM. No provider wrapping the app. No context value threading through the component tree. No cascading re-renders from a context change at the root. The hook is twenty-seven lines. It initialises the state to dark, reads the current attribute on mount, creates a MutationObserver filtered to the data-theme attribute, and disconnects the observer on cleanup. The attributeFilter option means the observer only fires for the one attribute it cares about — not every class change, not every style update, just theme switches. The ThemeToggle itself is equally simple. It reads the initial theme from localStorage, toggles between dark and light on click, sets the attribute on the document element, and persists the choice. The toggle and the hook never import each other. They communicate through the DOM — the toggle writes the attribute, the hook reads it. This pattern scales without modification. Adding a new themed component means calling useTheme and reading the return value. No provider to update, no context shape to extend, no barrel export to maintain. The DOM is the state bus.

software
Building

The Stripe payment iframe matches the page because it reads the same CSS variable

Every service page on the site sets a CSS custom property called --service-accent on its outermost container. Software pages set it to purple. Audio pages set it to cyan. Production pages set it to emerald. Accessibility pages set it to fuchsia. Musician pages set it to orange. Every component inside the page — the sidebar, the form, the navigation, the product cards — reads that variable for accent-coloured text, borders, and backgrounds. The Stripe payment iframe needs to match. The CheckoutWizard passes an appearance object to the Elements provider with five tokens. The colorPrimary is set to var(--service-accent, #60a5fa) — the CSS variable with a blue fallback in case the variable is missing. The colorBackground is the same near-black used across the dark theme. The colorText matches the site's text colour. The colorDanger is red-400. The borderRadius is eight pixels. The fontFamily is set to inherit. Six lines configure the entire payment form's visual identity. When a client checks out on the audio page, the payment form highlights in cyan. On the software page, it highlights in purple. The Stripe iframe reads the CSS variable at render time and applies it to the input focus states, the submit button, and the card brand icons. No conditional logic in the component. No service-to-colour mapping. The same appearance object works for all five services because the CSS variable is already set by the page above it. The theme property is set to night — Stripe's built-in dark mode — so the iframe background, text, and input styling align with the site's dark-first design. Light mode overrides in globals.css swap the accent colours but the Stripe Elements config does not need to change because the CSS variable resolves to the light variant automatically.

software
Building

The booking list queries future and past in parallel with one time boundary

The portal booking page shows two sections. Upcoming bookings at the top sorted by start time ascending — the next meeting first. Past bookings below sorted by start time descending — the most recent first. The query function creates a single Date object and uses it as the boundary for both queries. Upcoming bookings have a startTime greater than or equal to now. Past bookings have a startTime less than now. Both queries run inside a Promise.all call so they execute in parallel against the database. The past query is capped at ten results because scrolling through months of old meetings has no value — the client only needs to reference recent ones. The upcoming query has no limit because every future booking matters. Both queries are scoped to the client's ID. The function first resolves the client record from their Clerk authentication ID, then uses that internal ID to filter both booking queries. If the client record does not exist, the function returns two empty arrays immediately without touching the database. The same pattern appears in other portal queries. Projects, sessions, goals, and invoices all resolve the client by clerkId first, then scope every subsequent query to that client's ID. No query in the portal can return data belonging to another client because every query chain starts with the same authentication lookup. The booking split is a small detail but it reflects a broader principle in the portal — do two fast queries in parallel rather than one flexible query that the frontend has to sort and split. The database handles the filtering. The component receives exactly the shape it needs.

software

If anything I've built or shared has been useful to you...

Buy me a coffee

Stay in the loop

Occasional updates on what I'm building, mixing tips, and accessibility insights. No spam.