Skip to content

Mobile & accessibility

A second brain must work everywhere and for everyone (epic #319, launch sequence #315). ZettelFlow is isDesktopOnly: false; this page is the standard the plugin holds itself to for the core thinking loop — capture → cultivate → advance state → view health → browse/install a system — plus the accessibility baseline for our custom UI, and the manual matrix a contributor walks before a release.

What the code guarantees

  • Segmented surface tabs are a real WAI-ARIA tablist: role="tablist" / role="tab" / role="tabpanel", aria-selected, aria-controls/aria-labelledby, roving tabindex, and arrow / Home / End keyboard navigation (ModeHostView).
  • Clickable note names across the surfaces (Home, Cultivate, Ask-your-graph, Reasoning paths, Agency review, Health, Resurface, Evidence map, the Evolution timeline) are keyboard-operable through makeActivatable — focusable, role="link", activated by click and Enter/Space (architecture/components/core/a11y.ts).
  • Every clickable note name also previews on a Ctrl/Cmd-hover (#594): the pointer sibling hoverPreview wires the same element to Obsidian's native Page preview popover, so you can peek at a note without leaving the surface. It is the core Page preview plugin, honouring its settings (including whether the modifier is required); with it disabled, hovering simply does nothing, and a gone note is neither linked nor previewable. Clicking still opens — the preview is an addition, never a replacement. The Lab is the one deliberate exception: it names its subject note and by design never shows it. test/architecture/components/hoverPreviewCoverage.test.ts fails the build on a surface that opens a note without offering the preview (or names why it is exempt).
  • The 3D graph degrades gracefully on mobile / when WebGL is unavailable: instead of a dead-end message it renders a navigable list of the same model (hubs first, live connection counts, each row a 44px button that opens the note). It never blanks or crashes.
  • Reduced motion — when the OS sets prefers-reduced-motion: reduce, the graph settles almost instantly (no long animated warmup, no directional particles) and our CSS transitions/animations are disabled globally.
  • Focus is visible (a :focus-visible ring on every namespaced widget) and touch targets on the tabs and fallback rows meet ~44px.
  • The note-builder wizard (#407) is a real listbox: the option list is role="listbox" with a single tab stop and aria-activedescendant, options are role="option" with aria-selected, and movement is decided by the pure reducer in application/components/select/optionListModel.ts — arrows with wrap, Home/End, PageUp/PageDown, Enter and Space, plus typeahead. The filter field is permanent and labelled (it used to appear only once you typed, which is undiscoverable), each step is announced through a polite live region, icons are aria-hidden and every icon-only control carries an aria-label.
  • Modals extend Obsidian's Modal, which traps focus and restores it to the prior element on close.

These are guarded by test/architecture/components/core/a11y.test.ts, test/application/components/optionListModel.test.ts and the source-scanning test/application/components/wizardConventions.test.ts, so they can't silently regress.

Manual release matrix

Automated tests can't stand in for a device. Before a release, walk this on a phone (or the mobile emulator) and with the keyboard only:

Check Pass when
Quick capture The title prompt opens, is comfortable to type in, writes to the Inbox.
Home Greeting, nudge, teasers and lists render; every note name opens on tap and on Enter.
Cultivate Target + moves render; connect/question/source inputs are usable; buttons are tappable.
Advance state The lifecycle command runs and writes the new state.
Health Bars and lists render without overflow; drill-downs open.
Systems Browse + one-click install writes the canvas + step notes.
Graph on mobile The navigable list fallback shows, hubs first, and rows open notes.
Note builder Arrows/Home/End/typeahead move the option list; Enter and Space both choose; each step is announced.
Keyboard only Tab reaches every control; arrow keys move between surface tabs; focus is always visible.
Reduced motion With the OS setting on, the graph does not animate for long and transitions are off.

Contributor expectations

When you add UI:

  • Prefer a real <button> for anything clickable. If you must use a <span>/<div>, wrap it with makeActivatable(el, onActivate) — never a bare el.addEventListener("click", …).
  • Give icons/emoji that carry meaning a text alternative (aria-label), or mark them aria-hidden when decorative.
  • Don't rely on hover for anything essential — hover doesn't exist on touch.
  • Keep tap targets ≥ 44px for primary actions; respect prefers-reduced-motion for any new animation.
  • Add or extend a guardrail in a11y.test.ts when you add an interactive surface.

Known follow-ups

The exhaustive keyboard-operability sweep of the remaining read-only lenses (Discovery, Open questions, Evidence map, Concept navigation, Knowledge dashboard) and the recorded device walkthrough are tracked in #325 (follow-up to epic #319).

Colour pairs (#431)

A strong background carries its foreground in the same rule, and a test enforces it (test/config/colourPairs.test.ts). The pairs in use:

Background Foreground Where
--color-accent / --color-accent-hover --text-on-accent the actions list header, a selected search result, the step badge in the gallery
--interactive-accent --text-on-accent primary buttons, the uninstall hover
--background-modifier-error --text-normal the developer section and the remove hover — a tint, not a solid fill

This exists because the actions list shipped unreadable: the header filled with --color-accent and stated no foreground, so the action's type — a link — fell back to --text-accent, the same hue family, and only became legible on hover. Taste cannot be linted; an unpaired strong background can.

Phase colours on the canvas (#429)

Colour on a ZettelFlow canvas means something: the phase of the knowledge arc a step advances. The map is one definition (zettelkasten/phases/phaseColor.ts), read by both the canvas and the wizard's option accent.

Phase Canvas preset Theme note
Capture 1 (red) Obsidian's own canvas palette; contrast is the theme's, not ours
Classify 2 (orange) "
Process 3 (yellow) the lightest preset — never the sole carrier (see below)
Connect 4 (green) "
Develop 5 (cyan) "
Review · Consolidate 6 (purple) two phases share the closing colour (seven phases, six presets)

Colour is never the only carrier. Every phased step also states its phase as text in the step editor and as a group heading in the wizard, and the node's badges (questions · template · linked note · optional · conditional exits) are words, not hues. The legend on the canvas states the map, including the shared colour, so the meaning is reachable without the docs.

Contrast itself is Obsidian's: the presets are the app's own --canvas-color-N variables, which the light and dark default themes define and keep legible. ZettelFlow adds no custom hue, so a theme that adjusts the canvas palette adjusts ours with it. The badges use --background-secondary / --text-muted, a pair that is theme-defined and covered by the colour pair test above.

README vocabulary for this page: Accessibility.