Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Soft Reader

A Firefox addon that applies a soft reader mode. Where the built-in reader mode throws the page away and rebuilds it from the article text, this one leaves the page exactly where it is and only raises a few typographic floors — and only on pages that need them. Every page is measured first, and one that already reads well is left completely untouched.

The floors:

  • Minimum font size — text is never rendered below your threshold.
  • Minimum contrast — text that fails a WCAG ratio against its real background is nudged toward black or white until it passes, keeping its hue.
  • Serif body text — with code, icon fonts, form controls and sites that already use a serif left alone.
  • Minimum line spacing — a small bonus, since cramped leading undoes most of the benefit of a larger font.

Nothing is moved, hidden or reparented. Every change is an !important inline style that is recorded and put back exactly as it was when you switch the addon off, so toggling is instant and lossless — no reload.

Installing

It is unsigned, so the permanent route needs Developer Edition, Nightly, or ESR (xpinstall.signatures.required=false in about:config). For everyday use on release Firefox, load it temporarily:

  1. Open about:debugging#/runtime/this-firefox
  2. Load Temporary Add-on…
  3. Pick manifest.json from this folder
  4. Grant site access. Under Manifest V3 Firefox does not hand out <all_urls> at install — until you allow it, the addon can see nothing. Open the popup and click Grant access to all sites, or set it from about:addons → Soft Reader → Permissions → Access your data for all websites.
  5. Reload any tab you want it to act on

Temporary addons are unloaded when Firefox restarts. To make it permanent, get it signed — see below.

Signing it for permanent install

Firefox will not permanently install an unsigned addon on release builds, but Mozilla will sign one for you without listing it publicly. Get an API key and secret from Manage API Keys, then:

WEB_EXT_API_KEY=user:12345:67 WEB_EXT_API_SECRET=your-secret npm run sign

web-ext reads any option from a matching WEB_EXT_* environment variable, so no credentials go on the command line or into a file. Automated review takes a few minutes and the signed .xpi lands in web-ext-artifacts/.

Install it with about:addons → the gear icon → Install Add-on From File. It survives restarts, and site permissions still have to be granted once.

Each upload needs a version AMO has not seen before, so bump version in manifest.json before re-signing.

Check the packaging before submitting:

npx web-ext lint --source-dir . --ignore-files "node_modules/**" "test/**" "package*.json" --self-hosted

Package it with:

zip -r -FS ../soft-reader.zip manifest.json icons src

Using it

  • The toolbar button opens a popup with a per-site on/off switch, the sliders you will actually reach for, and what the addon decided about this page — either why it stepped in, or the measurements that persuaded it not to.
  • Alt+Shift+R toggles the current site without opening anything.
  • Full settings, including the serif stack and your list of per-site exceptions, live in the addon's options page (Settings, from the popup).

The button's badge reads:

Badge Meaning
(none) Running, and it changed this page
ok (green) Running, but this page already reads well — nothing changed
off (grey) Disabled on this site

If Firefox tucked the button into the » overflow or the puzzle-piece extensions menu, right-click it there and choose Pin to Toolbar.

Deciding whether a page needs help

The read pass has to happen anyway, so measuring the page before touching it is free. What it buys is restraint: on a well-set page the addon does nothing at all, and the toolbar button shows a green ok.

A page is judged on the text you actually read — runs of at least 40 characters, weighted by length, so a cramped navigation bar cannot condemn a well-set article and one faint caption cannot either. It needs help if:

  • its body text (the length-weighted median size) is below your minimum, or
  • more than 20% of its text, by character, fails your contrast ratio, or
  • its lines are set tighter than 1.25.

Judging a page is deliberately stricter than fixing one. A floor exists to be applied wherever it is breached, but tipping a whole page into reader treatment should take a real problem rather than a near miss — which is why the leading bar for judging is 1.25 while the one for fixing defaults to 1.5. Pages with under 250 characters are not judged at all; there is nothing to go on.

Once a page is judged to need help, the full treatment applies to it, not just the failing part. That keeps the result coherent — a page half in one typeface and half in another reads worse than either.

Late-arriving content is re-judged. A page that starts as an empty shell and renders its article a second later is treated when the article shows up, so client-rendered sites are not permanently written off on the strength of an empty first pass.

Turn all of this off with Only step in when a page needs it in Settings, which is what you want if you simply prefer serif text everywhere. To keep the assessment but override it for one site, use Apply here anyway in the popup.

How it works

src/content/soft-reader.js walks the document — including shadow roots and iframes — and picks out elements that directly hold text. It reads every decision for the whole page before writing anything, so a parent's new font size cannot leak into a child's em-relative size and get scaled twice.

Sizing. If body text is under the minimum, the whole page is scaled by the same factor (capped, default 1.5×) so headings keep their relative weight, with the minimum applied afterwards as a hard floor. Turn off Scale the page proportionally to clamp small text only and leave everything else untouched.

Contrast. For each text element, effectiveBackground() walks outwards compositing translucent background layers until it hits something opaque, or the canvas. If it meets a background image or gradient on the way it returns nothing and the text is left alone by default, because the real backdrop cannot be measured and guessing at it looks worse than the original. Translucent text is composited over its backdrop before being measured. The correction itself bisects toward black or white for the smallest shift that clears the ratio, so colour survives: a burnt orange stays a burnt orange, just a darker one. Large text is held to a lower ratio, as WCAG allows.

Typeface. The serif is skipped when the computed stack is already a serif or looks like an icon font, and never applied under <code>, <pre>, <kbd>, <samp>, <var> or form controls.

A MutationObserver handles content that arrives later. New nodes are measured inside an already-restyled page, so they are clamped to the minimum rather than scaled again.

<html> carries data-soft-reader="on" when the page was treated and "standby" when it was judged fine and left alone, if you want to hang your own userstyles off either.

Layout

Path Purpose
manifest.json MV3, Firefox 140+ (142+ on Android).
src/lib/settings.js Defaults, storage helpers, per-site resolution. Shared by every context.
src/lib/color.js Parsing, alpha compositing, WCAG luminance, contrast correction.
src/content/soft-reader.js The traversal, measurement and styling passes.
src/background/background.js Toolbar badge and keyboard shortcut. An MV3 event page, so it keeps no state between events.
src/popup/ Quick controls.
src/options/ Full settings, with a live preview.
test/ Node tests. Nothing here ships in the addon.

Tests

npm install && npm test

color.test.js checks the contrast maths directly: that corrections land on the target ratio without overshooting, that translucent text is composited first, that hue survives, and that an unreachable target degrades to the better of black or white instead of looping.

settings.test.js pins the per-site resolution rules — the master switch, the default, and per-site exceptions — including that an exception matching the default is discarded so the list stays meaningful.

content.test.js runs the real content script inside jsdom against a stubbed browser.* API, covering traversal (shadow roots, skipped tags, icon fonts), the per-site rules, and — the one that matters most — that disabling the addon restores every element's original inline style exactly. It also drives the assessment from both sides: comfortable, slightly-loose and one-faint-caption pages must come out untouched, while small, faint and cramped ones must be treated, and an empty shell that renders small text a second later must flip.

jsdom's cascade is thin: it reports font-size: medium for elements with no explicit size, and the content script correctly declines to act on those, so test fixtures set sizes explicitly. jsdom is pinned because older versions silently drop !important on some properties.

Known limits

  • Text drawn on background images or gradients is skipped by default. You can turn that off in Settings, which falls back to measuring against white.
  • ::before / ::after content cannot be given inline styles, so generated text keeps the site's own sizing.
  • An ancestor opacity or filter changes the rendered colour in ways the contrast pass does not model.
  • Resolved backgrounds are cached for the life of a pass, and class changes are not watched, so a site's own light/dark toggle will not re-trigger the contrast pass. Toggle the addon off and on to re-measure.
  • Sites with heavy client-side rendering may briefly show their own styling before the pass lands.

About

SoftReader Addon for Mozilla Addons - A gentle readability addon that kicks in only for the worst sites

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages