Accessibility · Component reference

Reading toolbar

Keep size, spacing, appearance and fullscreen controls close to the text.

Published and reviewed · Starlit’s implementation notes

Try the live example

Research and our take

Our recorded research compared reader preferences and accessibility tools across several projects. For this component, these observations informed Starlit’s approach:

  • EvansWiki preferences gear: Native gear dialog, page shade, text size, line spacing, live preview, fullscreen and local reset. Our decision: Preserve the compact reading first model and contrast aware shade generation.
  • ATbar: A modular reading toolbar, text size, colored reading surfaces and speech tools. Our decision: Provide a focused toolbar and an optional local voice player.
  • MediaWiki accessibility for reading: Reader choices for typography and appearance, with presentation integrated into the site. Our decision: Adapt a designated content region instead of filtering the entire page.

Starlit wrote the shared controller, scoped content adapters and component modules presented here. The referenced widgets are research sources, not browser dependencies. The component also draws on interface work in EvansWiki and PayCal.

Design reasoning

Make the most frequent reading changes available close to long content.

  1. Start with the person’s task and the barrier they may encounter.
  2. Use above an article or reader. Choose this small toolbar when a full preferences panel would interrupt the reading task.
  3. Keep the ordinary content and action usable before adding enhancement.
  4. Increase and decrease size to both bounds. Try fullscreen, exit with Escape, and confirm the article remains readable at a narrow width.

Usage pattern

Use above an article or reader. Choose this small toolbar when a full preferences panel would interrupt the reading task.

Impact and tradeoffs

Nearby controls reduce travel. Too many repeated toolbars add keyboard stops and visual noise.

Accessibility solutions

Use named size buttons and native sliders. Fullscreen is optional and must report unavailable support without blocking reading.

Build accessible source content first. Personal preferences supplement semantic HTML, useful names, keyboard access and visible focus.

HTML

A concrete usage example. Host these assets on your own origin if you adapt the pattern. Read the source and credits before reuse.

<link rel="stylesheet" href="/open-ui/accessibility/shared/content.css">
<script type="module"
  src="/open-ui/accessibility/components/reading-toolbar/index.js"></script>

<article id="reading" data-sdou-a11y-content data-default-theme="dark">
  <div data-sdou-a11y-prose>
    <h2 id="begin">Begin with a clear purpose</h2>
    <p>Readable text and useful controls belong together.</p>
    <h2 id="details">Consider the details</h2>
    <p>The page should remain usable before any enhancement loads.</p>
  </div>
</article>
<sdou-reading-toolbar target="#reading"
  storage-key="example.reading-toolbar.v1"></sdou-reading-toolbar>

Open the HTML usage source

CSS

This is the component stylesheet used inside its Shadow DOM. The shared ui.css supplies its base controls and content.css adapts the reading region. An empty component stylesheet deliberately inherits the shared UI.

.size-stepper {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 12px;
  align-items: center;
}
.size-stepper output {
  text-align: center;
  font:
    1.6rem Georgia,
    serif;
  color: var(--sdou-ax-gold);
}

JavaScript

This is the actual component module used by the live example. Imports, the base class and shared preferences are dependencies. Keep the component folder and shared folder in their relative positions.

import { A11yComponent, range, select, preview } from "../../shared/base.js?v=sdou20261029";
class ReadingToolbar extends A11yComponent {
  static styles = new URL("./component.css?v=sdou20261029", import.meta.url).href;
  render() {
    this.mount(
      "A few useful reading controls.",
      "Keep the essentials close, with clear actions and a live view of the result.",
      '<div class="size-stepper"><button type="button" class="button secondary" data-smaller aria-label="Decrease reading text size">A−</button><output data-size aria-live="off"></output><button type="button" class="button secondary" data-larger aria-label="Increase reading text size">A+</button></div>' +
        range("leading", "Line spacing", 1.5, 2.5, 0.05, [
          "Comfortable",
          "Spacious",
        ]) +
        select("theme", "Appearance", [
          ["system", "Use device setting"],
          ["dark", "Starlit dark"],
          ["light", "Starlit light"],
          ["warm", "Warm paper"],
        ]) +
        preview +
        '<button type="button" class="button secondary" data-fullscreen>Fullscreen reading</button>',
    );
  }
  initialize() {
    this.on(this.q("[data-smaller]"), "click", () =>
      this.preferences.set({
        size: Math.max(
          1,
          Math.round((this.preferences.state.size - 0.05) * 100) / 100,
        ),
      }),
    );
    this.on(this.q("[data-larger]"), "click", () =>
      this.preferences.set({
        size: Math.min(
          2,
          Math.round((this.preferences.state.size + 0.05) * 100) / 100,
        ),
      }),
    );
    const button = this.q("[data-fullscreen]");
    button.disabled = typeof this.target.requestFullscreen !== "function";
    if (button.disabled)
      this.say(
        "Fullscreen is unavailable in this browser. Reading controls still work.",
      );
    this.on(button, "click", async () => {
      try {
        if (document.fullscreenElement === this.target)
          await document.exitFullscreen();
        else await this.target.requestFullscreen();
      } catch {
        this.fail(
          "Fullscreen is unavailable here. You can keep reading on this page.",
        );
      }
    });
    this.on(document, "fullscreenchange", () => {
      button.textContent =
        document.fullscreenElement === this.target
          ? "Exit fullscreen"
          : "Fullscreen reading";
    });
  }
  update() {
    const size = this.preferences.state.size;
    if (this.q("[data-size]"))
      this.q("[data-size]").value = Math.round(size * 100) + "%";
    if (this.q("[data-smaller]")) this.q("[data-smaller]").disabled = size <= 1;
    if (this.q("[data-larger]")) this.q("[data-larger]").disabled = size >= 2;
  }
}
customElements.define("sdou-reading-toolbar", ReadingToolbar);

PHP integration

In your own application, PHP can render this HTML on the server. Here, the example is escaped text only. The library has no PHP runner or execution endpoint. The browser owns the interaction.

<?php
// Server render trusted structure and escape authored text.
// PHP does not implement or save these browser preferences.
declare(strict_types=1);
$title = 'A readable article';
$paragraph = 'Your authored content goes here.';
$e = static fn (string $value): string =>
    htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
?>
<link rel="stylesheet" href="/open-ui/accessibility/shared/content.css">
<script type="module"
  src="/open-ui/accessibility/components/reading-toolbar/index.js"></script>
<article id="reading" data-sdou-a11y-content data-default-theme="dark">
  <div data-sdou-a11y-prose>
    <h2 id="begin"><?= $e($title) ?></h2>
    <p><?= $e($paragraph) ?></p>
  </div>
</article>
<sdou-reading-toolbar target="#reading"
  storage-key="example.reading-toolbar.v1"></sdou-reading-toolbar>

Open the PHP rendering source

API and dependencies

Element
sdou-reading-toolbar
target
A selector for the content region. Use a unique ID. Components sharing a target share its controller.
storage-key
A stable key for this project. Use the same key for controls targeting the same region. Preferences stay in browser storage when available.
Content
data-sdou-a11y-content marks the target; data-sdou-a11y-prose identifies reading copy. Use data-default-theme="dark" for a dark initial and reset preference.
Event
sdou:a11y-change is dispatched on the target with preferences, palette and reducedMotion in its detail.
Dependencies
Modern JavaScript modules, custom elements, Shadow DOM and native form controls. Dialog, speech, clipboard and fullscreen support depend on the feature and browser.

Read shared/base.js · Read the preference controller · Read the content adapter · Read the shared control styles

Testing and limitations

Increase and decrease size to both bounds. Try fullscreen, exit with Escape, and confirm the article remains readable at a narrow width.

The original library validation records automated rule checks, 320px reflow, enlarged text, text spacing, state persistence and relevant functional checks. Unresolved automated Shadow DOM reference checks are recorded separately from browser dialog checks. These historical results are not a new audit of every integration.

Production review still needs assistive technology, touch, zoom, custom styles and representative content. See our testing scope and known gaps. Report a barrier to Starlit.