Accessibility · Component reference

Accessibility help

Create a local issue report with linked field errors and optional preference diagnostics.

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:

  • Royal Access: Typography, visual preferences and access to accessibility information. Our decision: Provide an accessible help and local report component. Keep content repair in author workflows.

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 it possible to describe a barrier with enough context to investigate it.

  1. Start with the person’s task and the barrier they may encounter.
  2. Use a local report builder with an explicit onward contact route. Include preference diagnostics only when the reader chooses them.
  3. Keep the ordinary content and action usable before adding enhancement.
  4. Submit an empty report, follow its field errors, prepare a valid report, and verify no request sends the report automatically.

Usage pattern

Use a local report builder with an explicit onward contact route. Include preference diagnostics only when the reader chooses them.

Impact and tradeoffs

Structured reports can improve reproduction. Asking for unnecessary personal or health details discourages useful feedback.

Accessibility solutions

Link errors to fields, keep status separate from errors, and clearly explain that preparing a report does not send it.

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

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.

#report-output {
  font:
    0.8125rem/1.65 ui-monospace,
    monospace;
  min-height: 260px;
}
#details-error:empty {
  display: none;
}

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, download } from "../../shared/base.js?v=sdou20261029";
class AccessibilityHelp extends A11yComponent {
  static styles = new URL("./component.css?v=sdou20261029", import.meta.url).href;
  render() {
    this.mount(
      "Tell us what got in the way.",
      "Prepare a useful report on this device. You choose whether to copy or download it. Nothing is sent automatically.",
      '<form novalidate><div class="stack"><label class="field-label" for="report-topic">What were you trying to do?</label><select id="report-topic"><option>Read the content</option><option>Use the keyboard</option><option>Adjust the appearance</option><option>Use audio or captions</option><option>Something else</option></select><label class="field-label" for="report-details">What happened?</label><textarea id="report-details" required aria-describedby="details-hint details-error" placeholder="Describe the step that was difficult and what you expected."></textarea><p class="hint" id="details-hint">Include the useful details. Leave out passwords, payment information and anything private.</p><p class="error" id="details-error"></p><label class="toggle" for="include-settings"><span class="toggle-copy"><strong>Include reading preferences</strong><small>Attach your current settings to help reproduce the problem.</small></span><input type="checkbox" id="include-settings"></label><button type="submit" class="button">Prepare local report</button></div></form><div class="stack section" data-result hidden><label class="field-label" for="report-output">Your report</label><textarea id="report-output" readonly rows="9"></textarea><div class="actions"><button type="button" class="button secondary" data-copy>Copy report</button><button type="button" class="button secondary" data-download>Download report</button></div><p class="hint">Review the report, then share it through your project’s usual support channel.</p></div><details><summary>About these reading tools</summary><div class="group-body"><p class="hint">These components give you control over presentation. Accessible content, semantic markup and testing with assistive technology remain part of the underlying product.</p></div></details>',
      { reset: false },
    );
  }
  initialize() {
    this.on(this.q("form"), "submit", (event) => {
      event.preventDefault();
      const field = this.q("#report-details");
      if (!field.value.trim()) {
        field.setAttribute("aria-invalid", "true");
        this.q("#details-error").textContent =
          "Describe what happened so the report is useful.";
        field.focus();
        return;
      }
      field.removeAttribute("aria-invalid");
      this.q("#details-error").textContent = "";
      const lines = [
        "Starlit accessibility report",
        "Topic: " + this.q("#report-topic").value,
        "Page: " + location.origin + location.pathname,
        "",
        "What happened:",
        field.value.trim(),
      ];
      if (this.q("#include-settings").checked)
        lines.push(
          "",
          "Reading preferences:",
          JSON.stringify(this.preferences.state, null, 2),
        );
      this.q("#report-output").value = lines.join("\n");
      this.q("[data-result]").hidden = false;
      this.say("Your report is ready to review. It has not been sent.");
      this.q("#report-output").focus();
    });
    this.on(this.q("[data-copy]"), "click", async () => {
      try {
        if (!navigator.clipboard?.writeText) throw Error("Unavailable");
        await navigator.clipboard.writeText(this.q("#report-output").value);
        this.say("Report copied.");
      } catch {
        this.q("#report-output").focus();
        this.q("#report-output").select();
        this.say(
          "Automatic copying is unavailable. The report is selected so you can copy it with your browser.",
        );
      }
    });
    this.on(this.q("[data-download]"), "click", () => {
      download(
        this.q("#report-output").value,
        "sdou-accessibility-report.txt",
      );
      this.say("Report prepared for download.");
    });
  }
}
customElements.define("sdou-accessibility-help", AccessibilityHelp);

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

Open the PHP rendering source

API and dependencies

Element
sdou-accessibility-help
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

Submit an empty report, follow its field errors, prepare a valid report, and verify no request sends the report automatically.

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.