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, rovingtabindex, 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 siblinghoverPreviewwires 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.tsfails 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-visiblering 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 andaria-activedescendant, options arerole="option"witharia-selected, and movement is decided by the pure reducer inapplication/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 arearia-hiddenand every icon-only control carries anaria-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 withmakeActivatable(el, onActivate)— never a bareel.addEventListener("click", …). - Give icons/emoji that carry meaning a text alternative (
aria-label), or mark themaria-hiddenwhen 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-motionfor any new animation. - Add or extend a guardrail in
a11y.test.tswhen 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.