User Personalization Accessibility Skill

SkillDev tools

Load this skill whenever the project has personalization features, user preference controls, theme selectors, font size adjusters, motion toggles, contrast settings, or any user-configurable accessibility accommodations. Under no circumstances override or ignore user OS-level accessibility preferences without explicit user consent. Absolutely always persist user preferences, apply them immediately, and respect prefers-reduced-motion, prefers-contrast, and similar media queries.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the User Personalization Accessibility Skill skill

What this skill tells your AI

The instructions your AI receives, as published by mgifford/accessibility-skills in skills/user-personalization/SKILL.md and read by ahel’s review.

Canonical source: examples/USER_PERSONALIZATION_ACCESSIBILITY_BEST_PRACTICES.md in mgifford/ACCESSIBILITY.md This skill is derived from that file. When in doubt, the example is authoritative.

Apply these rules when implementing user preference controls or reviewing existing personalization features. Only load this skill if the project has personalization features.


Core Mandate

Personalization is an enhancement to an accessible foundation — not a repair layer and not a substitute for WCAG conformance. Start by respecting OS, browser, and AT settings; add site-specific controls only when they provide a meaningful choice users cannot reliably get from those existing tools. The page must still satisfy browser zoom, text resizing, text-spacing overrides, forced colors, and keyboard operation whether or not it offers a preference editor.

Preference precedence model: (1) start with an accessible default; (2) follow a recognized system/browser preference when the site setting is "System"/"Default"; (3) let an explicit site choice override only the corresponding presentation dimension; (4) let "Reset to defaults" remove site overrides and resume following the system. Do not interpret a media-query match as a diagnosis — people choose dark mode, reduced motion, or increased contrast for many reasons.


Severity Scale (this skill)

LevelMeaning
CriticalOverlay used as compliance substitute; interferes with user's AT
SeriousOS-level preferences (reduced-motion, colour scheme) not respected
ModeratePersonalization widget not keyboard accessible; state not announced
MinorPreferences not persisted; prefers-reduced-data not considered

Critical: Never Use Accessibility Overlays as a Compliance Substitute

Third-party "accessibility overlay" widgets that claim to auto-fix issues must not be used as a compliance substitute. Using an overlay as a compliance claim is Critical — they cannot fix underlying structural issues (missing labels, incorrect ARIA, tab order), actively interfere with users' own AT (screen readers, browser zoom, OS high contrast), provide false compliance assurance, and are widely rejected by the disability community (see https://overlayfactsheet.com/).

A preference editor is different from an overlay: it lets a user choose among presentations the site has deliberately designed and tested. Personalization must not: claim to make an otherwise inaccessible site conformant; rewrite semantics generically at runtime; interfere with AT or user styles; conceal unresolved defects; or replace remediation in source templates. Runtime remediation for a locked legacy system can be a documented, tested, temporary risk mitigation — not conformance, and not a reason to stop underlying remediation work.


Required Outcome: Separate WCAG Requirements From Enhancements

WCAG usually requires content to adapt to user settings — it does not generally require a site to reproduce browser settings in a custom panel.

RequirementWhat authors must actually do
1.4.4 Resize Text (AA)Don't prevent user-agent resizing; content/controls stay usable at 200%. A site text-size control is one technique, not a universal requirement.
1.4.12 Text Spacing (AA)Allow user overrides of line/paragraph/letter/word spacing without loss of content or function — the site does not have to provide spacing controls
1.4.10 Reflow (AA)Preserve content/functionality at the required narrow width
Contrast / non-text contrastEvery author-provided theme or preset must independently meet the applicable criteria
Reduced motionRespect prefers-reduced-motion where motion may create a barrier; a site control can add an additional override
Reading mode, font choice, densityUseful enhancements when research demonstrates need — not automatic WCAG requirements

Do not claim that adding a preference panel makes inaccessible content conformant.


Serious: Respect User Preference Media Features (Progressive Enhancement)

Unsupported queries are ignored, so the default presentation must remain accessible without them.

Color scheme

:root {
  color-scheme: light dark;
  --page-background: #ffffff;
  --text-color: #1f2937;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --page-background: #111827;
    --text-color: #f3f4f6;
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --page-background: #111827;
  --text-color: #f3f4f6;
}

Absence of data-theme means "System"; an explicit selection sets the attribute. See skills/light-dark-mode/SKILL.md for the complete theme-selector pattern.

Reduced motion

Do not apply a universal rule forcing every animation/transition to 0.01ms !important — that pattern can break state changes and scripted event assumptions that rely on transitionend/animationend firing. Instead, remove or replace specific non-essential motion:

.panel { transition: translate 200ms ease-out, opacity 200ms ease-out; }

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion]) .panel { transition: none; }
  :root:not([data-motion]) { scroll-behavior: auto; }
}
:root[data-motion="reduce"] .panel { transition: none; }

If motion communicates essential state, replace it with an immediate state change — don't remove the information.

Contrast preferences

prefers-contrast can express more, less, or custom — do not treat every match as a request for high contrast:

@media (prefers-contrast: more) {
  :root { --border-width: 0.1875rem; --muted-text-color: currentColor; }
}
@media (prefers-contrast: less) {
  .decorative-texture { background-image: none; }
}
@media (prefers-contrast) {
  .decorative-gradient, .decorative-shadow { background-image: none; box-shadow: none; }
}

An unqualified prefers-contrast query is appropriate for reducing visual complexity generally — it is not appropriate for imposing a specific high-contrast palette, since the user may have requested less contrast or a custom palette.

Reduced transparency and forced colors

@media (prefers-reduced-transparency: reduce) {
  .dialog-backdrop { background: Canvas; opacity: 1; }
}
@media (forced-colors: active) {
  .button {
    color: ButtonText;
    background: ButtonFace;
    border: 0.125rem solid ButtonText;
  }
  .button:focus-visible { outline: 0.1875rem solid Highlight; }
}

Keep the default forced-color-adjust: auto; use forced-color-adjust: none only for a narrowly scoped element with its own tested response — it is not a general focus-indicator fix. Media Queries Level 5 remains a W3C Working Draft — treat newer features as evolving, not a replacement for a tested default.


Moderate: Use an Accessible Preference Editor

Prefer an inline <details> disclosure when options are few — this avoids modal focus-management requirements:

<details class="display-preferences">
  <summary>Display preferences</summary>
  <form id="display-preferences-form">
    <fieldset>
      <legend>Color theme</legend>
      <label><input type="radio" name="theme" value="system" checked> System</label>
      <label><input type="radio" name="theme" value="light"> Light</label>
      <label><input type="radio" name="theme" value="dark"> Dark</label>
    </fieldset>
    <label for="text-size">Text size</label>
    <select id="text-size" name="textSize">
      <option value="default">Default</option>
      <option value="large">Large</option>
    </select>
    <button type="button" id="reset-display-preferences">Reset to defaults</button>
    <p id="display-preferences-status" role="status"></p>
  </form>
</details>

Native radio buttons and selects already expose selected values — don't add a live-region announcement for every visual change unless testing shows it's necessary; a concise status message when saved/reset/unsaved is enough. Label the entry point by its purpose ("Display preferences", "Reading settings"), not "Accessibility" — no single icon communicates every possible preference, so keep a visible text label even if an icon is present.

If a modal is genuinely necessary, implement the complete pattern: accessible name, initial focus, contained tab sequence, Escape dismissal, visible close button, focus return to opener.


Moderate: Apply Presentation With CSS Variables, Not Raw Stored Strings

:root { --content-measure: 80ch; --reading-line-height: 1.5; }
:root[data-text-size="large"] { font-size: 125%; }
:root[data-reading-layout="comfortable"] { --content-measure: 70ch; --reading-line-height: 1.7; }
.reading-content { max-inline-size: var(--content-measure); }

Use relative sizing so browser zoom and user font settings keep working — root percentages scale from the user's default rather than replacing it. Avoid fixed-height text containers or clipping that fail when font/text size/language/spacing changes.


Serious: Validate, Apply, and Persist Values Safely

localStorage is unavailable in private browsing, cross-origin iframes, and storage-restricted environments — always wrap access in try/catch. Validate every stored value against an allowlist before applying it — do not insert stored strings directly into CSS or class names.

const preferenceKey = "site.displayPreferences.v1";
const defaults = Object.freeze({ theme: "system", textSize: "default", motion: "system" });
const allowedValues = Object.freeze({
  theme: ["system", "light", "dark"],
  textSize: ["default", "large", "x-large"],
  motion: ["system", "reduce"]
});

function sanitizePreferences(candidate = {}) {
  const source = candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : {};
  return Object.fromEntries(
    Object.entries(defaults).map(([name, defaultValue]) => {
      const value = source[name];
      return [name, allowedValues[name].includes(value) ? value : defaultValue];
    })
  );
}

function readPreferences() {
  try {
    const stored = localStorage.getItem(preferenceKey);
    return stored ? sanitizePreferences(JSON.parse(stored)) : { ...defaults };
  } catch {
    return { ...defaults };
  }
}

function setOptionalAttribute(name, value, defaultValue) {
  if (value === defaultValue) document.documentElement.removeAttribute(name);
  else document.documentElement.setAttribute(name, value);
}

function applyPreferences(preferences) {
  setOptionalAttribute("data-theme", preferences.theme, "system");
  setOptionalAttribute("data-text-size", preferences.textSize, "default");
  setOptionalAttribute("data-motion", preferences.motion, "system");
}

Version the storage key (.v1) and migrate or discard old data deliberately when the schema changes — don't try to interpret obsolete values. Explain persistence accurately: say "saved on this device," not "follows you everywhere," unless account sync actually provides that. Handle storage denial/corruption safely and always provide a working Reset.


Serious: Persist Preferences Without Creating a Privacy Profile

Store only the values needed to reproduce the chosen presentation — never a presumed diagnosis or disability. Do not send preference values to analytics merely because they're available; do not expose preferences in public profiles; make account sync intentional and transparent; protect synced settings as account data; document retention/cookie use under the project's privacy process. User-preference media features can contribute to browser fingerprinting — prefer CSS responses over reading/transmitting values through JavaScript when possible.


Moderate: Reduce Motion and Distraction Without Removing Meaning

Reduced motion isn't the same as removing all visual change. Prefer: immediate state changes instead of movement across the viewport; crossfades where opacity alone isn't problematic; paused decorative animation; manual controls for carousels; no smooth scrolling; stable control positioning. If moving/blinking/scrolling/auto-updating content meets WCAG 2.2.2's conditions, provide Pause/Stop/Hide or update-frequency control — don't hide these controls inside the same motion-heavy component. A site-level "Reduce motion" setting can supplement prefers-reduced-motion; "System" must keep responding when the OS setting changes.


Moderate: Font Choices and Reading Adaptation

Evidence does not support naming one font as universally best for dyslexia or any disability — do not tell users which font they need based on a disability; let them choose based on the result they can see. If offering a font choice: describe neutrally (Sans serif/Serif/Monospace); ensure full language/character coverage; provide robust fallbacks; preserve user-agent minimum font-size settings; provide Default and Reset.

A reading layout may constrain line length, increase spacing, remove decorative backgrounds, reduce density, or reorder primary vs. supplementary content — but must never silently remove instructions, warnings, error messages, or required task information. If a setting hides optional material, tell users what's hidden and provide an immediate way to restore it. W3C COGA guidance and WAI-Adapt are supplemental resources, not additional WCAG 2.2 conformance requirements.


Keep Every Theme and Preset Independently Accessible

Every author-provided theme must independently meet: text contrast; non-text contrast; focus visibility/not-obscured; visited-link distinction; selected/ checked/expanded/current/error states; chart/icon legibility; forced-color behavior; print output. Do not label a site preset "High contrast" unless it's a defined, tested, complete theme — and don't imply it's equivalent to the OS forced-colors mode. "Higher contrast" is often the more accurate label.


Progressive Enhancement and Failure Handling

The unenhanced page must remain readable and operable: use CSS media queries even when a site control is also present; use native HTML controls before custom widgets; don't require third-party scripts for basic access; don't block rendering on remote preference services; preserve system preferences if storage fails; keep reset/recovery available after a partial failure.


Minor: prefers-reduced-data

An emerging media query for serving lighter content on metered connections. Support is currently limited — gate behind @supports or feature detection and treat as forward-looking, not a current requirement.

@media (prefers-reduced-data: reduce) {
  .decorative-hero-image { display: none; }
  video[autoplay] { display: none; }
}

Testing

  • Default/failure states: test before JS runs and with JS disabled; block local storage and confirm settings still apply for the current page; insert malformed/obsolete stored values and confirm they're ignored; test first visit, returning visit, Reset, sign-out; confirm storage/network failure doesn't block content
  • System preferences: test light/dark schemes and changing scheme while the page is open (System mode should follow); test reduced motion before and after load; test prefers-contrast values (more/less/custom); test at least one light and one dark forced-color palette
  • Site controls: operate every setting with keyboard, touch, pointer, and speech input; confirm labels/groups/values/focus/status with a screen reader; confirm applying a setting doesn't move focus or navigate unexpectedly; confirm Reset returns presentation and controls to defaults
  • Content/layout: resize text to 200%; test 400% zoom and required narrow width; apply all four Text Spacing override values; confirm controls, tables, diagrams, and messages remain available
  • Combinations: test individual settings first, then high-risk combinations (extra-large text + comfortable layout, dark theme + reduced motion, explicit theme + forced colors, 400% zoom with panel open, stored preferences after a schema update) — use pairwise/risk-based coverage, not exhaustive combination testing

Automated checks can verify some contrast, labels, states, storage behavior, and target sizes — cannot determine whether a preference is understandable, whether combinations create cognitive overload, or whether an injected remediation conflicts with AT. Include people who use relevant personalization/AT features in usability testing.


Common Failures

FailureCorrection
A preference widget is claimed to make the site WCAG conformantRemediate the source; describe the widget only as optional personalization
WCAG 1.4.12 said to require spacing controlsTest that user overrides work without loss of content/functionality — the site need not provide the controls
A text-size button treated as a replacement for browser zoomKeep zoom enabled and support text resizing independently
All animations/transitions forced to 0.01ms !importantRemove or replace specific non-essential motion without breaking state changes
prefers-contrast assumed to always mean high contrastHandle more, less, and custom accurately
forced-color-adjust: none applied to custom focus indicatorsLet forced colors apply by default; use system colors where needed
A site "High contrast" theme treated as forced-color modeTest as an author theme; label its effect accurately
Every page adds A+/A-, font, color, speech, and nav controlsAdd only researched settings that can be maintained and tested
A settings icon is the only labelUse visible text: "Display preferences"
Stored values inserted directly into classes or CSSValidate against a small allowlist before applying
Local storage assumed permanent and universalHandle denial/clearing; describe device/browser scope accurately
Preference values used to infer disabilities or enrich analyticsStore the minimum presentation state only
A reading mode silently removes warnings or instructionsPreserve required content; disclose any hidden optional material
Only individual options are testedTest high-risk combinations, reset, migration, and failure states

Definition of Done Checklist

  • No third-party accessibility overlay used as a compliance substitute
  • Base experience is accessible without the preference editor
  • Browser zoom, text resizing, user styles, extensions, AT are not blocked
  • CSS media queries implemented: prefers-reduced-motion (targeted, not blanket 0.01ms), prefers-color-scheme, prefers-contrast (more/less/custom), forced-colors
  • Any widget labelled "preferences"/"display settings", not "accessibility"
  • Explicit site choices override only the corresponding setting; System/Default follows the OS
  • localStorage access wrapped in try/catch; stored values validated against an allowlist
  • Preferences restored safely on load; storage key versioned
  • Reset clears site overrides and updates controls
  • Personalization controls are keyboard accessible with native semantics
  • Preference data is minimized and never used to infer disability
  • Every theme/preset independently meets contrast and state requirements
  • Individual settings and high-risk combinations manually tested

Key WCAG Criteria

  • 1.4.3 Contrast Minimum (AA)
  • 1.4.4 Resize Text (AA)
  • 1.4.10 Reflow (AA)
  • 1.4.11 Non-text Contrast (AA)
  • 1.4.12 Text Spacing (AA) — primary criterion this skill addresses
  • 1.3.6 Identify Purpose (AAA) — connects personalization to cognitive accessibility
  • 2.2.2 Pause, Stop, Hide (A)
  • 2.3.3 Animation from Interactions (AAA)
  • 2.4.11 Focus Not Obscured Minimum (AA)

References

Standards horizon: WCAG 3.0 is in development; personalization requirements are expected to expand, not contract. Monitor: https://www.w3.org/TR/wcag-3.0/

Signals

GitHub stars
44
Forks
2
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
user-personalization
Source
github.com/mgifford/accessibility-skills