Skip to content
All posts

6 min readCase study, Reading UX, Craft

Told in Passing: two story readers, one shelf

Avoidant Book, a calm reader for one therapeutic chapter, and Fake Love, a short story with its transactional analysis one tap away — what each one does differently, and why they now share a single codebase.


Told in Passing is a small library of short stories, each in its own quiet, mobile-first reader. It started as two separate apps: Avoidant Book, built around a single therapeutic chapter, and Fake Love, a short story with a layer of notes on the transactional-analysis concepts at work in it. Same rule for both, and for this write-up: the text is the client. We describe the readers, not the stories.

Stories, each its own reader
2
Annotations in Fake Love
25
Self-tests per story
7
Network calls
0

Avoidant Book: a reader for exactly one chapter

Some products are a platform; this one began as a single chapter, written in the Ericksonian storytelling tradition and kept anonymous. The brief was humility: the code promises the chapter is included verbatim, and every decision follows from treating the text as the client. It reads a section per page with gentle transitions, a sticky header carrying the current section and a progress bar, prev and next in three placements, and bottom sheets for the contents and settings on phones. Search filters the section list itself, a “key parts” switch narrows to the exercises and core passages, and font size, spacing, dark mode and your place all persist. No accounts, no analytics. The chrome is deliberately colorless so nothing competes with the words.

Because the chapter lives in the code as one long template literal, a bad paste or an overzealous find-and-replace could corrupt it silently. So the reader checks itself on every load: sections parse, expected titles exist, and a known phrase from inside the text still exists, a canary. A failure logs a warning naming the broken check; it never takes the page down. The markdown renderer builds React nodes directly and never touches dangerouslySetInnerHTML, so it is XSS-safe by construction. The first version went from a single-file chat artifact to a deployed Next.js app in 84 minutes.

Fake Love: the mechanics, one tap away

Fake Love took the same reader and added the one thing Avoidant Book never needed: annotations. Phrases in the text are wrapped in inline markers, {{key:phrase}}, and each key points to a separate record holding a title, one of nine concept categories, and the note. Writers edit prose that still reads as prose; reviewers read every note in one place. With the layer on, a marked phrase renders as a focusable button, color-coded by category, that opens a note card positioned beside it. One switch renders the plain text instead, not the same markup hidden with CSS.

The note card is clamped to a 16px gutter, flips below the phrase when there is no room above, and closes on a second tap, on Escape, or the moment the reading context changes, so it never floats over text that has moved out from under it. Search and “copy story” always see clean text. And because two data sets now have to agree, the self-tests check them both ways: every marker has a note, and every note is used by a marker. Today that is 25 and 25, one to one.

Why they share one codebase now

Two copies of one reader meant every fix landed twice, and the copies had already drifted: Fake Love had dropped its animation library while Avoidant Book still shipped it. So in October 2026 they merged into Told in Passing. Each story became a data module — its text, its identity, which sections count as key parts, its canaries, and for Fake Love the annotation record — and a single reader renders whichever one it is given. The annotation controls appear only for a story that has notes, so Avoidant Book reads exactly as plainly as before.

Merged is not blended. Each story keeps its own address, favicon, link preview and saved settings. The front door became a contents page: each story listed with its real chapters, every chapter a deep link into the reader, and one live Fake Love highlight you can tap before deciding to read. The merge also paid down what both copies owed: settings moved to useSyncExternalStore (no state set inside effects, a fallback when storage is blocked, and sync across tabs), a new self-test flags any key-parts rule that stops matching a section, the progress bar became a labeled progressbar, and every transition switches off under reduced motion. The old addresses redirect.

Two readers that differ where a reader would notice, and agree everywhere a reader wouldn't.

What we'd tell a client

  • Single-purpose apps earn disproportionate polish: one story with great controls beats a library with mediocre ones.
  • If content is embedded in code, test the embedding. A canary phrase is one line and catches silent corruption.
  • Keep annotations as inline keys pointing to a separate record, and when two data sets must match, test the match both ways.
  • When a second product reuses a first, merge them before the copies drift: make the difference data, not a fork.