This article explains the design decisions behind the Foblex Flow accessibility layer — why keyboard navigation drives the selection instead of a focus cursor, how a canvas full of absolutely-positioned nodes becomes readable to a screen reader, and why creating a connection from the keyboard turned out to be the easy part.
If you try to make a node editor accessible, you'll quickly face a problem:
That last one is the interesting problem, and it is the one this article is really about.
Most UI accessibility patterns assume a roving focus: arrow keys move document.activeElement from item to item, and selection is a separate thing you do to the focused item with Space or Enter.
I tried that first. And threw it away.
On a canvas it produces two competing highlights — the focus ring on one node, the selection style on another — and everyone gets confused, sighted users included. Worse, moving DOM focus into consumer-rendered nodes has side effects the library cannot predict: focusin handlers fire in your templates, :focus-within styles trigger, screen readers start reading node internals mid-navigation.
What shipped in v19 is the opposite arrangement:
f-flow host — permanently. It is the single tab stop.aria-activedescendant on the host.📌 In short: there is no keyboard focus state to learn. There is only selection, and the keyboard drives it.
This is the second canonical ARIA pattern (aria-activedescendant composite widgets) rather than the first (roving tabindex), and for a canvas it is the right one: no focus jumps, no consumer-template side effects, no double speech.
Arrow keys need an answer to "what is to the right of this node?" — and centers are the wrong way to compute it. A tall node whose center is far below still starts right next to you; a wide neighbor in the same row should win over a nearer one two rows down.
The spatial algorithm works on edges: a candidate qualifies when it lies ahead of the current item in the pressed direction, and it is scored by the gap between facing edges plus a doubled penalty for leaving the current row or column. The nearest diagonal neighbor beats a far straight one — on a sparse canvas, strict "straight sector first" logic (the TV-remote model) jumps across the whole graph, and that is not what a hand on the arrow keys expects.
Two more decisions matter here:
Ctrl+arrow adds the graph-native move: follow the connection in that direction to the node on its other end. Geometry navigation answers "what is near me"; topology navigation answers "what am I wired to".
Movement uses a grab pattern with two entry styles. Hold Space and use arrows — release to drop. Or tap Space to grab and tap again to drop. The second form exists because holding one key while pressing another is a chord, and chords are exactly what some motor-impaired users cannot do. Both forms move the whole selection — the same set a pointer drag would move — and emit the same fMoveNodes event on drop. Escape puts everything back.
Creating a connection is where the v19 architecture paid off. The click-to-connect feature shipped earlier in this release extracted a gesture-independent engine — FCreateConnectionSession — that owns the preview line, snapping, connectable marking, target resolution, and the fCreateConnection emission. The keyboard flow is just a third gesture on that engine:
C starts a session from the selected node (exactly one node must be selected — anything else would guess the source silently).Enter emits fCreateConnection. Escape cancels.Every connection rule — disabled connectors, categories, fCanBeConnectedTo, multiplicity — applies unchanged, because it is the same engine the pointer uses.
The golden rule of Foblex Flow did not move an inch: the library never mutates your data. Delete emits fDeleteSelected with the selected ids. This happened. You decide what to do.
The layer is split deliberately:
aria-hidden on the minimap and previews. These are inert attributes — and any attribute you set yourself is never overridden.provideFFlow(withA11y()). Every f-flow app older than v19 has its own key handling, because the library had none. A default-on layer would double-drive selection and deletion in all of them. So it doesn't.contenteditable or any focusable custom widget stay where they belong. Single-character shortcuts yield to OS combos — Ctrl+C stays copy.Click the canvas or Tab to it, then: arrows to select, Shift+arrow to extend, Ctrl+arrow to follow a connection, Space to move, C → Enter to connect, Delete to remove.
The accessibility layer is not a separate mode bolted onto the editor. It is the same selection model, the same events, and the same connection engine the pointer uses — driven by different keys. That is why it took a redesign to get right, and why the result feels like the editor, not like an add-on.
If you're building a visual editor in Angular and want a native Angular solution (not a React wrapper) — take a look.
And if you like what I'm building, please consider starring the repo ⭐
It helps the project a lot.