Hand-written HTML and CSS. No framework, no build step, no dependency tree, no package manager. Every accessibility feature described below is running on the page you are reading right now.
- Source
- Flat HTML, one stylesheet, one script
- Dependencies
- None
- Build step
- None
- Hosting
- GitHub Pages, static
- JavaScript
- ~120 lines, progressive
- Started
- 2025, still maintained
The stack, such as it is
Every page is a flat .html file at the repository root. They share one
styles.css and one main.js, loaded with defer. There
is no bundler, no template engine, and no generated output — what is in the repository is
what the browser receives.
That is a deliberate choice rather than a limitation I have not yet outgrown. At this size, a build step would add a dependency tree to audit, a lockfile to keep current, and a failure mode between writing a change and seeing it. What it would buy in return — component reuse across nine pages — I currently pay for by editing the nav in nine places. That trade flips somewhere around a dozen pages; see Limits.
Accessibility, and why each piece is there
-
A skip link as the first focusable element on every page. It is
positioned off-screen and pulled back with
:focus, not hidden withdisplay:none— a display-hidden element cannot receive focus at all, which defeats the purpose. -
Semantic landmarks with
aria-labelledbyon every section, pointing at that section's real heading. A screen-reader user gets a landmark list that reads like the page's table of contents rather than "region, region, region." -
Visible
:focus-visibleoutlines rather than suppressed ones. Removing focus rings is the single most common accessibility regression on portfolio sites, and it makes a page unusable by keyboard while looking fine in a screenshot. -
prefers-reduced-motionhonored on the smooth scroll, the cursor-tracked glow, and the section-target animation. The glow is removed entirely rather than slowed, because a cursor-following element is the specific thing that triggers discomfort for some readers. - Colour is never the only signal. Links carry an underline on hover and focus; the accent is decoration on top of structure, not a replacement for it.
-
Explicit
width/heightanddecoding="async"on images, so the layout does not shift as they load.
The theming system
Colour lives entirely in CSS custom properties. There are two independent axes, and keeping them independent is the whole idea:
-
Light or dark is set on the root element as a
data-modeattribute. A tiny inline script in every<head>applies the stored or system preference before first paint — if it ran in the deferred script instead, the page would flash dark before switching to light. -
The accent is a class on
<body>. Each project page carries its own: green for the site itself, burgundy for Knights Counsel, sky for Build-Recipe, gold for PocketProfessors, aqua for FitnessFunctions, lime for the Contact Manager.
One subtlety worth recording, because it cost me an afternoon: a custom property that
references another one resolves where it is declared, not where it is used. Writing
--accent-soft: var(--accent) at :root would freeze it to the root
accent and silently ignore the per-page theme. Everything derived from
--accent therefore has to be declared inside the body.theme-*
block, not at :root.
Light mode also needs more than an inverted background. Neon accents sit at roughly 1.3:1
against white, so each theme declares a separate --accent-ink — a darkened
version used for text and links that clears WCAG AA — while the bright value stays for
glows and borders where contrast does not apply.
The JavaScript
About 120 lines, and the page works without it. It does four things: fills the footer year,
toggles colour mode and persists the choice, collapses the nav bar on downward scroll, and
drives the email dropdown. The scroll handler is passive and coalesced through
requestAnimationFrame; the cursor-glow handler returns early when
prefers-reduced-motion matches, so the listener is never attached at all.
Limits, and when I would change my mind
Being honest about where this approach stops working is more useful than defending it.
- The nav bar is duplicated in every file. Nine copies today. It is the clearest signal that a template layer is coming — at around a dozen pages the editing cost overtakes the cost of a build step, and I would reach for a static site generator rather than a framework.
- No automated accessibility testing. The keyboard pass is manual, which means it is only as reliable as my memory to run it.
- The logo flip is hover-only, so it does not fire on touch devices. It is decorative and nothing depends on it, but it is a gap.
- Four glow effects share one idea. The cursor halo, the heading glow, the button glow, and the logo halo are variations on the same trick. That is one or two more than the design needs, and restraint would read better than repetition.