Accessibility · Component reference
Accessibility center
A complete gear panel with reading, appearance, motion and focus preferences.
Try the live exampleResearch 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.
- Fluid UI Options: Separate adjusters for type, contrast, spacing, links, inputs and table of contents. Our decision: Build small independent panels backed by a shared preference controller.
- Azkhar accessibility preference widget: Presentation preferences, reading aids, local storage and isolated widget styles. Our decision: Use isolated component styles and explicit target adapters.
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
Keep personal reading choices together without making them a prerequisite for using the page.
- Start with the person’s task and the barrier they may encounter.
- Use one gear near the main navigation, targeting the whole readable region. Preserve the existing controller on an established site.
- Keep the ordinary content and action usable before adding enhancement.
- Open the gear with Enter. Change shade and text size, close with Escape, reload, then reset. Check focus return and that real content changes.
Usage pattern
Use one gear near the main navigation, targeting the whole readable region. Preserve the existing controller on an established site.
Impact and tradeoffs
A single predictable entry point reduces clutter. A large panel adds discovery work, so group related controls and keep defaults usable.
Accessibility solutions
Give the dialog a name, move focus inside it, contain Tab navigation, support Escape and return focus to the gear. Preferences cannot repair missing semantics.
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/preferences-center/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-preferences-center target="#reading"
storage-key="example.preferences-center.v1"></sdou-preferences-center>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.
/* Local presentation adapter for the shared Starlit preferences center. */
:host {
display: block;
color-scheme: dark;
}
.launcher {
width: 44px;
min-width: 44px;
min-height: 44px;
height: 44px;
padding: 10px;
border: 0;
background: transparent;
color: var(--studio-gold, #e8c58e);
border-radius: 10px;
}
.launcher:hover:not(:disabled) {
background: var(--studio-panel, #102330);
color: var(--studio-ink, #f2e7d4);
border: 0;
}
.launcher > span {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.launcher svg {
width: 22px;
height: 22px;
}
dialog {
font-size: 1rem;
}
dialog .panel-footer {
position: static;
}
@media (max-width: 600px) {
dialog .dialog-head {
position: static;
}
.dialog-head h2 {
font-size: 1.4rem;
}
}
/* Keep the live page visible around the preferences panel. */
dialog {
width: min(520px, calc(100vw - 2px));
max-height: calc(100dvh - 2px);
margin: 1px 1px 1px auto;
}
dialog::backdrop {
background: transparent;
backdrop-filter: 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,
group,
typography,
appearance,
motion,
focus,
preview,
} from "../../shared/base.js?v=sdou20261029";
class PreferencesCenter extends A11yComponent {
static styles = new URL(
"./component.css?v=sdou20261029",
import.meta.url,
).href;
render() {
this.resetAll = true;
this.mount(
"Your reading space",
"Make the page feel comfortable. Every setting is optional and stays on your device.",
group("Appearance", appearance(), true) +
group("Text and spacing", typography()) +
group("Motion and media", motion()) +
group("Focus and navigation", focus()) +
preview +
'<p class="hint">Reading text contrast: <strong class="number" data-contrast-ratio></strong>. Our minimum is 5:1.</p>',
{ dialog: true },
);
this.q("[data-reset]").textContent = "Reset all preferences";
// Canonical presentation, promoted from the existing sltd.ca full center.
this.q(".kicker")?.remove();
this.q("[data-pref=theme]")?.closest(".control").remove();
this.q("[data-pref=font] option[value=studio]").textContent =
"Studio default";
const button = this.q("[data-open]");
button.setAttribute("aria-label", "Accessibility preferences");
button.title = "Accessibility preferences";
const motionGroup = this.qa("details").find(
(el) => el.querySelector("summary").textContent === "Motion and media",
);
const pause = document.createElement("button");
pause.type = "button";
pause.className = "button secondary";
pause.textContent = "Pause page media";
pause.dataset.pausePageMedia = "";
this.on(pause, "click", () => {
this.preferences.pauseMedia();
this.say(
"Page audio and video paused. Playback stays under your control.",
);
});
motionGroup.querySelector(".group-body").append(pause);
}
sync() {
super.sync();
if (!this.preferences) return;
const p = this.preferences.colors;
for (const [name, value] of Object.entries({
bg: p.page,
panel: p.panel,
raised: p.panel,
hover: p.panel,
ink: p.text,
muted: p.muted,
gold: p.accent,
teal: p.secondary,
line: p.line,
error: p.text,
}))
this.style.setProperty("--sdou-ax-" + name, value);
for (const [name, value] of Object.entries({
gold: p.accent,
panel: p.panel,
ink: p.text,
}))
this.style.setProperty("--studio-" + name, value);
this.style.colorScheme = p.dark ? "dark" : "light";
const slider = this.q("[data-pref=brightness]"),
output = this.q("[data-output=brightness]");
if (slider && output) {
output.value =
"Shade " + (Math.round(Number(slider.value)) + 1) + " of 32";
slider.setAttribute("aria-valuetext", output.value);
}
}
update() {
const output = this.q("[data-contrast-ratio]");
if (output)
output.textContent = this.preferences.colors.minimum.toFixed(2) + ":1";
}
}
customElements.define("sdou-preferences-center", PreferencesCenter);
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/preferences-center/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-preferences-center target="#reading"
storage-key="example.preferences-center.v1"></sdou-preferences-center>
API and dependencies
- Element
sdou-preferences-center- 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-contentmarks the target;data-sdou-a11y-proseidentifies reading copy. Usedata-default-theme="dark"for a dark initial and reset preference.- Event
sdou:a11y-changeis dispatched on the target withpreferences,paletteandreducedMotionin 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
Open the gear with Enter. Change shade and text size, close with Escape, reload, then reset. Check focus return and that real content changes.
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.