Accessibility · Component reference

Reading presets

Choose practical combinations, compare active settings and move preferences between devices.

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:

  • React accessibility widget: Grouped preferences, preset combinations and modal focus management. Our decision: Offer practical named presets with visible descriptions and individual overrides.
  • ExtenderX accessibility tools: Preference combinations, readable text, visible headings and reduced motion. Our decision: Use reversible descriptive presets without inferring a disability or making safety claims.

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 common combinations easier to try and move, while keeping every individual setting reversible.

  1. Start with the person’s task and the barrier they may encounter.
  2. Offer descriptive presets and explicit JSON export or import. Use the same storage key for components sharing a target.
  3. Keep the ordinary content and action usable before adding enhancement.
  4. Export settings, change them, import the file and compare. Try malformed JSON and a blocked storage environment, then reset.

Usage pattern

Offer descriptive presets and explicit JSON export or import. Use the same storage key for components sharing a target.

Impact and tradeoffs

Presets reduce setup effort. Labels should describe presentation rather than assume a diagnosis or promise an outcome.

Accessibility solutions

Validate imported size, version, keys and values. Reset only this project’s key, and keep the current visit usable if storage fails.

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/preference-profiles/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-preference-profiles target="#reading"
  storage-key="example.preference-profiles.v1"></sdou-preference-profiles>

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.

/* Reading presets. Component specific layout, shared controls live in shared/ui.css. */

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.

Read the javascript implementation (111 lines)
import { A11yComponent, download, escape } from "../../shared/base.js?v=sdou20261029";
export const presets = [
  {
    name: "Comfortable reading",
    description: "Larger type, more space and a shorter line.",
    values: { size: 1.25, leading: 1.9, letter: 0.02, word: 0.04, measure: 60 },
  },
  {
    name: "Low glare",
    description: "A dark surface with clear type and strong links.",
    values: {
      theme: "dark",
      brightness: null,
      contrast: "standard",
      links: true,
    },
  },
  {
    name: "Calmer motion",
    description: "Less movement, clear focus and quieter decoration.",
    values: { motion: "reduce", focus: true, hideDecorative: true },
  },
  {
    name: "Open spacing",
    description: "Generous word, letter and paragraph spacing.",
    values: {
      size: 1.1,
      leading: 1.75,
      letter: 0.12,
      word: 0.16,
      paragraph: 2,
      font: "sans",
    },
  },
];
class PreferenceProfiles extends A11yComponent {
  static styles = new URL("./component.css?v=sdou20261029", import.meta.url).href;
  render() {
    this.resetAll = true;
    this.mount(
      "Start with a comfortable combination.",
      "Presets describe what changes. Choose one, then adjust any setting to suit you.",
      '<div class="profile-grid">' +
        presets
          .map(
            (p, i) =>
              `<button class="profile" type="button" data-profile="${i}" aria-pressed="false"><strong>${escape(p.name)}</strong><small>${escape(p.description)}</small></button>`,
          )
          .join("") +
        '</div><div class="section stack"><h3>Move your preferences.</h3><p class="hint">Export a small JSON file, or import one you saved earlier. The file contains settings only.</p><button type="button" class="button secondary" data-export>Export preferences</button><label class="field-label" for="import-preferences">Import preferences</label><input id="import-preferences" type="file" accept=".json,application/json"><p class="hint" data-active-summary></p></div>',
    );
  }
  initialize() {
    this.q("[data-reset]").textContent = "Reset all preferences";
    this.qa("[data-profile]").forEach((button) =>
      this.on(button, "click", () => {
        const p = presets[Number(button.dataset.profile)];
        this.preferences.set(p.values);
        this.say(
          p.name + " applied. You can change individual settings at any time.",
        );
      }),
    );
    this.on(this.q("[data-export]"), "click", () => {
      download(
        this.preferences.export(),
        "sdou-reading-preferences.json",
        "application/json",
      );
      this.say("Preferences file prepared for download.");
    });
    this.on(this.q("#import-preferences"), "change", async (event) => {
      this.fail("");
      const file = event.target.files?.[0];
      if (!file) return;
      try {
        if (file.size > 10000)
          throw Error("Choose a preferences file smaller than 10 KB.");
        this.preferences.import(await file.text());
        this.say("Preferences imported and applied.");
      } catch (error) {
        this.fail(
          error instanceof SyntaxError
            ? "The file is not valid JSON. Choose a Starlit preferences file."
            : error.message,
        );
      }
      event.target.value = "";
    });
  }
  update() {
    const s = this.preferences.state;
    for (const button of this.qa("[data-profile]")) {
      const active = Object.entries(
        presets[Number(button.dataset.profile)].values,
      ).every(([key, value]) => s[key] === value);
      button.setAttribute("aria-pressed", String(active));
    }
    const summary = this.q("[data-active-summary]");
    if (summary)
      summary.textContent =
        "Current text size " +
        Math.round(s.size * 100) +
        "%, line spacing " +
        s.leading.toFixed(2) +
        " and " +
        s.measure +
        " characters per line.";
  }
}
customElements.define("sdou-preference-profiles", PreferenceProfiles);

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/preference-profiles/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-preference-profiles target="#reading"
  storage-key="example.preference-profiles.v1"></sdou-preference-profiles>

Open the PHP rendering source

API and dependencies

Element
sdou-preference-profiles
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

Export settings, change them, import the file and compare. Try malformed JSON and a blocked storage environment, then reset.

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.