AUG 27, 2026
An interactive dependency graph with no JavaScript, via :has()
Highlighting a node's neighbourhood looks like an event-listener problem. It is a selector problem, and CSS grew the selector — two generated rules per node, zero client JavaScript.
The graph draws five registries and every relationship between them. Hovering a node dims everything outside its neighbourhood. That reads like a job for an event listener and a state atom, and it is not one.
The trick is that a node's neighbourhood is knowable at build time. So each node's closed neighbourhood gets a shared class, and one rule per node raises it back out of the dim:
/* Anything hovered or focused dims the whole canvas... */
.eg:has(.eg-node:hover) .eg-node,
.eg:has(.eg-node:hover) .eg-edge { opacity: 0.22 }
/* ...and the hovered node's own neighbourhood comes back. */
.eg:has(#n-project_zckr_dev:hover) .nb-project_zckr_dev,
.eg:has(#n-project_zckr_dev:focus-visible) .nb-project_zckr_dev { opacity: 1 }:has() is what makes it work: the container can style itself, and therefore
every one of its descendants, based on a state living several levels down.
Before it, "when that is hovered, change this" only worked if this came
after that in document order — which a graph's edges never do.
Two details earned their place. :focus-visible beside :hover means the
nodes are <a> elements and tabbing through the graph walks the
neighbourhoods in order, so the keyboard path is the same interaction rather
than a lesser one. And the whole thing is inline <style> generated by the
component that draws the SVG, because the selectors name node ids that only
exist once the content has been loaded — the rules and the markup are emitted
together and cannot disagree.
What it replaced was never written: a force-directed canvas. That would have cost a client island on a site whose architectural claim is a fixed roster of eight, produced a different picture on every load — which makes the visual baselines flap and makes the graph impossible to talk about, since "the node on the left" means nothing if it moves — and answered a question nobody had asked. Thirty-five boxes and thirty-seven lines do not need physics.
One thing axe caught that I would have shipped: the <svg> had
role="img" with an aria-label, which is the reflex for a diagram. But
role="img" makes an element a leaf in the accessibility tree, and the
nodes inside it are links — so every one of them was a nested-interactive
violation. A <title> as the first child names an SVG without flattening what
is inside it, and each link gets its own <title> carrying the full label,
since the drawn text is clamped to fit the box and would otherwise be read
aloud with its ellipsis.