jShell: WCAG
(function () {
"use strict";
const STORAGE_KEY = "a11y-widget-settings";
const FOCUSABLE = 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';
const defaultSettings = {
fontScale: 1,
lineSpacing: false,
highContrast: false,
invertColors: false,
grayscale: false,
underlineLinks: false,
dyslexiaFont: false,
reduceMotion: false,
bigCursor: false,
highlightFocus: false,
};
let settings = loadSettings();
let lastFocusedBeforeOpen = null;
function loadSettings() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? { ...defaultSettings, ...JSON.parse(raw) } : { ...defaultSettings };
} catch (e) {
return { ...defaultSettings };
}
}
function saveSettings() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch (e) {}
}
function announce(msg) {
const live = document.getElementById("a11y-live-region");
if (live) {
live.textContent = "";
requestAnimationFrame(() => { live.textContent = msg; });
}
}
function injectStyles() {
const style = document.createElement("style");
style.id = "a11y-widget-styles";
style.textContent = `
#a11y-skip-link {
position: absolute;
left: -9999px;
top: 0;
z-index: 1000000;
background: #000;
color: #fff;
padding: 0.75rem 1rem;
font-family: system-ui, sans-serif;
text-decoration: none;
border-radius: 0 0 6px 0;
}
#a11y-skip-link:focus { left: 0; }
#a11y-live-region {
position: absolute;
width: 1px; height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
#a11y-header {
position: fixed !important;
top: 0.5rem !important;
right: 0.5rem !important;
left: auto !important;
width: auto !important;
z-index: 999997 !important;
display: block !important;
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
}
#a11y-toggle-btn {
display: inline-flex !important;
width: auto !important;
max-width: max-content !important;
align-items: center !important;
gap: 0.3rem !important;
background: #0b5fcc !important;
color: #fff !important;
border: 1px solid #66aaff !important;
border-radius: 6px !important;
padding: 0.25rem 0.55rem !important;
margin: 0 !important;
cursor: pointer !important;
font-family: system-ui, sans-serif !important;
font-size: 12px !important;
line-height: 1.4 !important;
box-shadow: 0 1px 4px rgba(0,0,0,0.4) !important;
}
#a11y-toggle-btn:hover,
#a11y-toggle-btn:focus-visible {
background: #0d70eb;
outline: 3px solid #ffb400;
outline-offset: 2px;
}
#a11y-overlay {
position: fixed;
inset: 0;
z-index: 999998;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
overflow-y: auto;
}
#a11y-overlay[hidden] { display: none; }
#a11y-dialog {
width: min(96vw, 900px);
max-height: 90vh;
overflow-y: auto;
background: #1a1a1a;
color: #fff;
border: 1px solid #555;
border-radius: 12px;
padding: 1.5rem;
font-family: system-ui, sans-serif;
box-shadow: 0 8px 30px rgba(0,0,0,0.6);
}
#a11y-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
#a11y-dialog-header h2 {
font-size: 18px;
margin: 0;
}
#a11y-close-btn {
background: #2e2e2e;
color: #fff;
border: 1px solid #555;
border-radius: 6px;
width: 32px;
height: 32px;
cursor: pointer;
font-size: 16px;
}
#a11y-close-btn:hover,
#a11y-close-btn:focus-visible {
background: #4a4a4a;
outline: 3px solid #ffb400;
outline-offset: 2px;
}
#a11y-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.6rem;
}
#a11y-grid button {
width: 100%;
background: #2e2e2e;
color: #fff;
border: 1px solid #555;
border-radius: 8px;
padding: 0.6rem 0.5rem;
cursor: pointer;
font-size: 13px;
text-align: center;
}
#a11y-grid button:hover,
#a11y-grid button:focus-visible {
background: #4a4a4a;
outline: 3px solid #ffb400;
outline-offset: 2px;
}
#a11y-grid button[aria-pressed="true"] {
background: #0b5fcc;
border-color: #66aaff;
}
#a11y-fontctrl {
display: flex;
align-items: center;
justify-content: space-between;
background: #2e2e2e;
border: 1px solid #555;
border-radius: 8px;
padding: 0.4rem 0.6rem;
margin-bottom: 0.6rem;
}
#a11y-fontctrl span { font-size: 13px; }
#a11y-fontctrl .a11y-fontbtns { display: flex; gap: 0.4rem; }
#a11y-fontctrl button {
background: #1a1a1a;
color: #fff;
border: 1px solid #555;
border-radius: 6px;
padding: 0.3rem 0.6rem;
cursor: pointer;
}
#a11y-fontctrl button:hover,
#a11y-fontctrl button:focus-visible {
background: #4a4a4a;
outline: 3px solid #ffb400;
outline-offset: 2px;
}
#a11y-reset-btn {
width: 100%;
margin-top: 1rem;
background: #5a1a1a;
color: #fff;
border: 1px solid #ff5555;
border-radius: 8px;
padding: 0.6rem;
cursor: pointer;
font-size: 13px;
}
#a11y-reset-btn:hover,
#a11y-reset-btn:focus-visible {
background: #732020;
outline: 3px solid #ffb400;
outline-offset: 2px;
}
html.a11y-underline-links a { text-decoration: underline !important; }
html.a11y-line-spacing body,
html.a11y-line-spacing body * {
line-height: 1.8 !important;
letter-spacing: 0.02em !important;
word-spacing: 0.05em !important;
}
html.a11y-high-contrast body { background: #000 !important; color: #fff !important; }
html.a11y-high-contrast a { color: #ffe86b !important; }
html.a11y-invert-colors { filter: invert(1) hue-rotate(180deg); }
html.a11y-invert-colors img,
html.a11y-invert-colors video { filter: invert(1) hue-rotate(180deg); }
html.a11y-grayscale { filter: grayscale(1); }
html.a11y-invert-colors.a11y-grayscale { filter: invert(1) hue-rotate(180deg) grayscale(1); }
html.a11y-dyslexia-font body,
html.a11y-dyslexia-font body * {
font-family: Verdana, Tahoma, sans-serif !important;
}
html.a11y-reduce-motion *,
html.a11y-reduce-motion *::before,
html.a11y-reduce-motion *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
html.a11y-big-cursor,
html.a11y-big-cursor * {
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path d="M3 2l18 9-8 2-3 8-7-19z" fill="black" stroke="white" stroke-width="1"/></svg>') 0 0, auto !important;
}
html.a11y-highlight-focus *:focus-visible {
outline: 4px solid #ffb400 !important;
outline-offset: 3px !important;
}
`;
document.head.appendChild(style);
}
function buildSkipLink() {
let target = document.querySelector("main, [role='main'], #main, #content");
if (!target) return;
if (!target.id) target.id = "a11y-main-content";
if (!target.hasAttribute("tabindex")) target.setAttribute("tabindex", "-1");
const link = document.createElement("a");
link.id = "a11y-skip-link";
link.href = "#" + target.id;
link.textContent = "Skip to main content";
link.addEventListener("click", () => { target.focus(); });
document.body.insertBefore(link, document.body.firstChild);
}
function buildLiveRegion() {
const live = document.createElement("div");
live.id = "a11y-live-region";
live.setAttribute("aria-live", "polite");
live.setAttribute("role", "status");
document.body.appendChild(live);
}
function buildHeader() {
const header = document.createElement("header");
header.id = "a11y-header";
const btn = document.createElement("button");
btn.id = "a11y-toggle-btn";
btn.type = "button";
btn.setAttribute("aria-haspopup", "dialog");
btn.setAttribute("aria-expanded", "false");
btn.setAttribute("aria-controls", "a11y-overlay");
btn.innerHTML = "♿ Accessibility";
header.appendChild(btn);
document.body.insertBefore(header, document.body.firstChild);
return btn;
}
function buildOverlay() {
const overlay = document.createElement("div");
overlay.id = "a11y-overlay";
overlay.hidden = true;
overlay.innerHTML = `
<div id="a11y-dialog" role="dialog" aria-modal="true" aria-labelledby="a11y-dialog-title">
<div id="a11y-dialog-header">
<h2 id="a11y-dialog-title">Accessibility Settings</h2>
<button type="button" id="a11y-close-btn" aria-label="Close accessibility settings">×</button>
</div>
<div id="a11y-fontctrl">
<span>Font Size</span>
<div class="a11y-fontbtns">
<button type="button" data-action="font-dec" aria-label="Decrease font size">A−</button>
<button type="button" data-action="font-inc" aria-label="Increase font size">A+</button>
</div>
</div>
<div id="a11y-grid">
<button type="button" data-toggle="lineSpacing" aria-pressed="false">Line Spacing</button>
<button type="button" data-toggle="highContrast" aria-pressed="false">High Contrast</button>
<button type="button" data-toggle="invertColors" aria-pressed="false">Invert Colors</button>
<button type="button" data-toggle="grayscale" aria-pressed="false">Grayscale</button>
<button type="button" data-toggle="underlineLinks" aria-pressed="false">Underline Links</button>
<button type="button" data-toggle="dyslexiaFont" aria-pressed="false">Readable Font</button>
<button type="button" data-toggle="reduceMotion" aria-pressed="false">Reduce Motion</button>
<button type="button" data-toggle="highlightFocus" aria-pressed="false">Highlight Focus</button>
<button type="button" data-toggle="bigCursor" aria-pressed="false">Big Cursor</button>
</div>
<button type="button" id="a11y-reset-btn">Reset All Settings</button>
<br><br>
<p>Im always trying my best to pursue WCAG. If anything misses, contact <a href="https://docs.jbreda.dev/contact.php">me</p>
</div>
`;
document.body.appendChild(overlay);
return overlay;
}
function wireEvents(toggleBtn, overlay) {
const dialog = overlay.querySelector("#a11y-dialog");
const closeBtn = overlay.querySelector("#a11y-close-btn");
toggleBtn.addEventListener("click", () => openOverlay(toggleBtn, overlay));
closeBtn.addEventListener("click", () => closeOverlay(toggleBtn, overlay));
overlay.addEventListener("click", (e) => {
if (e.target === overlay) closeOverlay(toggleBtn, overlay);
});
overlay.addEventListener("click", (e) => {
const t = e.target.closest("button");
if (!t) return;
const toggleKey = t.getAttribute("data-toggle");
const action = t.getAttribute("data-action");
const id = t.id;
if (toggleKey) {
settings[toggleKey] = !settings[toggleKey];
applySettings();
announce(t.textContent.trim() + (settings[toggleKey] ? " enabled" : " disabled"));
return;
}
if (action === "font-inc") {
settings.fontScale = Math.min(2, +(settings.fontScale + 0.1).toFixed(2));
applySettings();
announce("Font size " + Math.round(settings.fontScale * 100) + " percent");
} else if (action === "font-dec") {
settings.fontScale = Math.max(0.7, +(settings.fontScale - 0.1).toFixed(2));
applySettings();
announce("Font size " + Math.round(settings.fontScale * 100) + " percent");
} else if (id === "a11y-reset-btn") {
settings = { ...defaultSettings };
applySettings();
announce("All settings reset");
}
});
dialog.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
closeOverlay(toggleBtn, overlay);
return;
}
if (e.key === "Tab") {
const focusables = Array.from(dialog.querySelectorAll(FOCUSABLE));
if (focusables.length === 0) return;
const first = focusables[0];
const last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
}
function openOverlay(toggleBtn, overlay) {
lastFocusedBeforeOpen = document.activeElement;
overlay.hidden = false;
toggleBtn.setAttribute("aria-expanded", "true");
const dialog = overlay.querySelector("#a11y-dialog");
const firstFocusable = dialog.querySelector(FOCUSABLE);
if (firstFocusable) firstFocusable.focus();
}
function closeOverlay(toggleBtn, overlay) {
overlay.hidden = true;
toggleBtn.setAttribute("aria-expanded", "false");
if (lastFocusedBeforeOpen) lastFocusedBeforeOpen.focus();
}
function applySettings() {
const html = document.documentElement;
html.style.fontSize = (settings.fontScale * 100) + "%";
html.classList.toggle("a11y-line-spacing", settings.lineSpacing);
html.classList.toggle("a11y-high-contrast", settings.highContrast);
html.classList.toggle("a11y-invert-colors", settings.invertColors);
html.classList.toggle("a11y-grayscale", settings.grayscale);
html.classList.toggle("a11y-underline-links", settings.underlineLinks);
html.classList.toggle("a11y-dyslexia-font", settings.dyslexiaFont);
html.classList.toggle("a11y-reduce-motion", settings.reduceMotion);
html.classList.toggle("a11y-big-cursor", settings.bigCursor);
html.classList.toggle("a11y-highlight-focus", settings.highlightFocus);
saveSettings();
syncButtonStates();
}
function syncButtonStates() {
document.querySelectorAll("#a11y-overlay [data-toggle]").forEach((btn) => {
const key = btn.getAttribute("data-toggle");
btn.setAttribute("aria-pressed", String(!!settings[key]));
});
}
function init() {
injectStyles();
buildSkipLink();
buildLiveRegion();
const toggleBtn = buildHeader();
const overlay = buildOverlay();
wireEvents(toggleBtn, overlay);
applySettings();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();