Open-source, framework-friendly guided fixation reading: the leading letters of every word are emphasised so the eye lands on an artificial fixation point and the brain completes the rest of the word.
Smooth reading works. → **Smo**oth **read**ing **wor**ks.
The technique is similar to commercial fixation-reading products, but this project is independent, clean-room, trademark-free, and licensed under Apache-2.0. The algorithm is documented in full in docs/SPEC.md, ensuring that every port produces identical output.
| Package | Registry | Platforms | Description |
|---|---|---|---|
@smooth-reading/core |
npm | Web, Node.js, React Native, Deno, Bun | Zero-dependency tokenizer, HTML transformer, DOM applier, streaming transform. |
SmoothReading |
Swift Package Manager | iOS 15+, macOS 12+, watchOS 8+, tvOS 15+, visionOS 1+ | Native Swift 6 library. NSAttributedString for UIKit/AppKit, AttributedString for SwiftUI. |
io.github.pythonshe:smooth-reading |
Maven Central | Android (minSdk 24), JVM | AnnotatedString for Jetpack Compose, Spanned for Android Views, pure JVM core. |
smooth-reading |
PyPI | Python 3.10+ | Zero-dependency Python library, type-annotated, with a built-in CLI. |
smooth_reading |
pub.dev | Dart 3.0+, Flutter (iOS, Android, web, desktop), server | Pure Dart. Sealed Token for TextSpan rendering, toHtml, toMarkdown, pluggable Segmenter for platform ICU. |
smooth-reading |
crates.io | Rust 1.85+ (std only, no unsafe) |
Borrowed Token<'a> for any text stack, to_html, to_markdown, Segmenter trait for ICU, smooth-reading CLI. |
pnpm add @smooth-reading/core
# npm install @smooth-reading/core · yarn add @smooth-reading/coreimport { toHtml } from "@smooth-reading/core";
toHtml("Smooth reading works.");
// '<b>Smo</b>oth <b>read</b>ing <b>wor</b>ks.'
toHtml("Smooth reading works.", { fixation: 5, saccade: 2, tag: "span", className: "sr-fixation" });
// '<span class="sr-fixation">Smoot</span>h reading <span class="sr-fixation">work</span>s.'Render directly from tokens without innerHTML (see docs/recipes/react.md):
import { tokenize } from "@smooth-reading/core";
export function SmoothText({ children, ...options }) {
return tokenize(children, options).map((token, index) =>
token.type === "word" && token.fixation > 0 ? (
<span key={index}>
<b>{token.fixationText}</b>
{token.restText}
</span>
) : (
token.text
)
);
}// Package.swift
.package(url: "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/PythonShe/Smooth_Reading.git", from: "0.3.0")import SmoothReading
// UIKit / AppKit convenience
label.applySmoothReading("Smooth reading works.")
// SwiftUI
Text(SmoothReading.attributedString("Smooth reading works.", options: SmoothOptions(fixation: 3)))// build.gradle.kts — resolves from Maven Central
implementation("io.github.pythonshe:smooth-reading:0.3.0")import io.smoothreading.SmoothReading
import io.smoothreading.android.annotatedString
import io.smoothreading.android.setSmoothText
// Jetpack Compose
Text(SmoothReading.annotatedString("Smooth reading works."))
// Android Views (TextView)
textView.setSmoothText("Smooth reading works.")pip install smooth-readingfrom smooth_reading import to_html
to_html("Smooth reading works.")
# '<b>Smo</b>oth <b>read</b>ing <b>wor</b>ks.'flutter pub add smooth_reading # or: dart pub add smooth_readingimport 'package:smooth_reading/smooth_reading.dart';
// Flutter: build TextSpans from tokens (full widget in docs/recipes/flutter.md)
TextSpan(children: [
for (final token in tokenize('Smooth reading works.'))
switch (token) {
WordToken(fixation: > 0, :final fixationText, :final restText) =>
TextSpan(children: [
TextSpan(text: fixationText, style: const TextStyle(fontWeight: FontWeight.w700)),
TextSpan(text: restText),
]),
WordToken(:final text) || SeparatorToken(:final text) => TextSpan(text: text),
},
]);
// Server / web: HTML
toHtml('Smooth reading works.');
// '<b>Smo</b>oth <b>read</b>ing <b>wor</b>ks.'cargo add smooth-readinguse smooth_reading::{to_html, tokenize, HtmlOptions, Options, Token};
let options = Options::default();
assert_eq!(
to_html("Smooth reading works.", &options, &HtmlOptions::default()),
"<b>Smo</b>oth <b>read</b>ing <b>wor</b>ks.",
);
// Tokens borrow from the input; render them with any text stack.
for token in tokenize("Smooth reading works.", &options) {
match token {
Token::Word { fixation_text, rest_text, .. } => print!("[{fixation_text}]{rest_text}"),
Token::Separator { text } => print!("{text}"),
}
}The crate also installs a smooth-reading command (cargo install smooth-reading) that reads a file or standard input and writes HTML or Markdown.
Existing open-source implementations are typically single-purpose string replacers, archived browser extensions, or GPL/AGPL licensed. Most split contractions, break bidirectional text, inject unescaped innerHTML, and fail completely on scripts without spaces.
Smooth Reading was built to provide an enterprise-grade, specification-backed foundation:
- Strict cross-platform parity: Governed by a formal specification (docs/SPEC.md). Every port passes the same shared test fixtures (
fixtures/common/), guaranteeing identical output across Web, iOS, macOS, Android, Python, Dart/Flutter and Rust. - Multilingual & Unicode fidelity: Correct dictionary word boundaries for non-space scripts (Chinese, Japanese, Thai, Lao, Khmer, Burmese), ligature-safe Indic conjuncts (Unicode 15.1 GB9c), non-destructive bidirectional (RTL) handling for Arabic and Hebrew, and grapheme cluster counting instead of naive code units.
- Modern UI integration: Framework-native primitives without unsafe HTML injection. Renders directly as React/Vue/Svelte components, Flutter
TextSpans, Jetpack ComposeAnnotatedString, UIKit/AppKitNSAttributedString, and SwiftUIAttributedString. - Zero runtime dependencies: Pure Apache-2.0 license, zero external dependencies across all ports, zero telemetry, and zero network calls. Runs completely offline.
Framework adapters are intentionally not published as separate packages; they are lightweight, copy-paste components built directly on @smooth-reading/core's tokenize(). They render native elements (avoiding innerHTML), making them safe for untrusted input and server-side rendering:
| Framework / Platform | Recipe Guide | Description |
|---|---|---|
| React / Next.js / Remix | docs/recipes/react.md | Pure components (RSC safe), TanStack Query hooks, and ref callbacks. |
| React Native / Expo | docs/recipes/react-native.md | Nested <Text> rendering compatible with Hermes and Expo. |
| Vue 3 | docs/recipes/vue.md | Computed tokenize() component and v-smooth directive. |
| Svelte 5 | docs/recipes/svelte.md | Runes-based component and use:smooth action. |
| Angular (17+) | docs/recipes/angular.md | Signals-based standalone component and directive. |
| Web Components / HTML | docs/recipes/web-component.md | Zero-build custom element and progressive DOM enhancement. |
| Markdown Pipelines | docs/recipes/markdown.md | Rehype plugins (Astro, Docusaurus, Next MDX) and pure Markdown transforms. |
Explore all recipes in docs/recipes/.
Controlled empirical studies (Readwise 2022, Snell 2024, Možina et al. 2025) found no reading-speed benefit from this technique for the general population, and no controlled study has demonstrated a benefit for ADHD or dyslexic readers. Many people nevertheless report that they subjectively prefer reading this way.
Treat it as an optional reading preference you can offer your users, not an objective speed-reading or cognitive enhancement feature. Scientific literature:
- Readwise reader study, 2022, ~1,900 participants: blog.readwise.io.
- Snell, Acta Psychologica (2024): sciencedirect.com.
- Možina, Kovačević & Blaznik, SAGE Open (2025): doi:10.1177/21582440251376158.
- Attention, Perception & Psychophysics (2025): doi:10.3758/s13414-025-03067-w.
- One documented algorithm: Shared JSON fixtures, identical math, and identical output across languages.
- Unicode first:
Intl.Segmenterand ICU word breaking for non-space scripts, grapheme-cluster counting, and respectful handling of diacritics and ligatures. - Safe by default: Automatically skips
<code>,<pre>,<script>,<style>,<kbd>,<samp>, and<textarea>; escapes all emitted text; avoidsinnerHTMLin UI recipes. - Presentation is CSS: The core emits neutral markup (
<b>or configurable tags/classes); weight, color, and opacity remain fully customizable via stylesheet custom properties. - Zero runtime dependencies: Self-contained, lightweight, and offline.
International scripts and right-to-left text are primary architectural requirements of this project, not an afterthought. Every port passes fixtures/common/scripts.json (Korean, Vietnamese, Hindi, Bengali, Tamil, Arabic, Hebrew, Persian, Urdu, Greek, Turkish, German, mixed-direction text, emoji); ICU-backed ports also pass fixtures/segmenter/ (Chinese, Japanese, Thai, Lao, Khmer, Burmese, bidi paragraphs). See docs/LANGUAGES.md for the detailed specification and engine comparisons.
| Script | @smooth-reading/core |
Swift | Android | Python | Dart | Rust | Status |
|---|---|---|---|---|---|---|---|
| Latin, Greek, Cyrillic, Vietnamese, Turkish, German | Intl.Segmenter (regex fallback) |
ICU | ICU or spec regex | spec regex | spec regex | spec regex | Identical everywhere |
| Korean (Hangul, spaces) | Intl.Segmenter (regex fallback) |
ICU | ICU or spec regex | spec regex | spec regex | spec regex | Identical; decomposed jamo compose in all ports |
| Chinese, Japanese | Intl.Segmenter dictionary |
ICU dictionary | android.icu dictionary |
One word per run | One word per run | One word per run | Dictionary breaks in ICU ports; predictable run rule in Python, Dart and Rust |
| Thai, Lao, Khmer, Burmese | Intl.Segmenter dictionary |
ICU dictionary | android.icu dictionary |
One word per run | One word per run | Dictionary breaks in ICU ports; predictable run rule in Python | |
| Devanagari, Bengali, Gujarati, Oriya, Telugu, Malayalam | Intl.Segmenter (regex fallback) |
ICU | ICU or spec regex | spec regex | spec regex | spec regex | Identical; conjuncts (क्ष) are one cluster (Unicode 15.1 GB9c) |
| Tamil and other Indic scripts | Intl.Segmenter (regex fallback) |
ICU | ICU or spec regex | spec regex | spec regex | spec regex | Identical; marks stay attached to base consonants |
| Arabic, Persian, Urdu, Hebrew | Intl.Segmenter (regex fallback) |
ICU | ICU or spec regex | spec regex | spec regex | spec regex | Identical; tashkeel and niqqud stay with base letters; Persian ZWNJ preserved in ICU ports |
Digits of any script (\p{Nd}) |
— | — | — | — | — | — | Unemphasised unless emphasizeNumbers: true; still count towards saccade |
| Emoji, modifiers, ZWJ sequences | — | — | — | — | — | — | Treated as separators; passed through untouched |
Bold is typographically weak for some scripts. Many Arabic, Devanagari, Bengali, and Thai typefaces lack a dedicated bold face or have a bold weight that barely differs from regular. Synthetic bolding can also blur intricate conjuncts and vowel marks. We recommend emitting semantic classes instead of <b> and styling fixations with color, weight, or opacity:
toHtml(text, { tag: "span", className: "sr-fixation", restTag: "span", restClassName: "sr-rest" });.sr-fixation { color: var(--sr-fixation-color, #1d4ed8); font-weight: 600; }
.sr-rest { opacity: var(--sr-rest-opacity, 0.85); }In Swift and Android, pass custom attributes directly: fixationAttributes in nsAttributedString / attributedString, or SpanStyle(color = …) in annotatedString(). In Rust, render tokenize() output with whatever styling your text stack offers.
The fixation point is always the logical start of the word (the first characters read). Arabic and Hebrew words are naturally emphasised on their right-hand side without requiring special directional wrappers.
Emitted markup and attributed strings never insert dir attributes, <bdi> elements, or artificial bidi control characters. Mixed-direction inputs (Hello مرحبا world, RTL sentences with embedded Latin terms and numbers) are tested in every port to ensure lossless roundtripping.
All six ports share one version number and one git tag (vX.Y.Z); see
CHANGELOG.md for release notes.
| Port | Where it is published | Current channel |
|---|---|---|
@smooth-reading/core |
npm | pnpm add @smooth-reading/core (0.3.0) |
SmoothReading |
GitHub releases (SwiftPM resolves git tags directly) and the Swift Package Index | from: "0.3.0" |
io.github.pythonshe:smooth-reading |
Maven Central | 0.3.0 |
smooth-reading |
PyPI | pip install smooth-reading (0.3.0) |
smooth_reading |
pub.dev | dart pub add smooth_reading (0.3.0) |
smooth-reading (Rust) |
crates.io | cargo add smooth-reading (0.3.0) |
The public API is described in docs/SPEC.md; it follows
semantic versioning from 0.3.0 on, with breaking changes only in minor
versions while the major is 0.
External pull requests are not accepted for now; bug reports, test cases, and questions via issues are warmly welcome, and forks are encouraged. See CONTRIBUTING.md.
# TypeScript core
pnpm install && pnpm build && pnpm test
# Swift package
cd swift && swift test
# Android & JVM
cd android && ./gradlew test
# Python package
cd python && python -m pip install -e ".[dev]" && pytest && mypy --strict smooth_reading
# Dart package
cd dart && dart pub get && dart analyze && dart test
# Rust crate
cd rust && cargo clippy --all-targets -- -D warnings && cargo testSee CONTRIBUTING.md for guidelines on maintaining multi-port parity and CHANGELOG.md for version release notes.
Apache-2.0. See LICENSE.