Email accessibility testing tools
Tools to help with the manual testing of HTML emails.
Automated accessibility testing software
Try one of these tools first:
Automated tools are useful for finding common problems, including many colour contrast issues. If you need to check colour contrast manually, try WebAIM’s
Use an automated checker first, then use this console snippet to inspect email-specific issues that automated tools may miss.
Testing with a console snippet (June 2026)
The console snippet is a small piece of code that you copy from this page and paste into Chrome.
It helps you review email-specific accessibility checks such as headings, links, keyboard tab-stops, image alt text, tables, email structure, and text spacing.
Use it after running an automated checker. This gives you a combination of automated testing and manual review.
Minified console snippet
(()=>{const e=window.A11Y_EMAIL=window.A11Y_EMAIL||{},t=e.ids={report:"a11y-email-report",styles:"a11y-email-styles",svg:"a11y-email-tabstops-svg",imageOverlay:"a11y-email-image-overlay",listOverlay:"a11y-email-list-overlay",headingsOverlay:"a11y-email-headings-overlay",overlay:"a11y-email-geometry-overlay"};e.defaults={headings:!1,links:!1,tabStops:!1,emailMeta:!1,images:!1,tables:!1,tableLinearise:!1,textSpacing:!1,baseFontSize:16},e.escapeHtml=e=>String(e??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'"),e.escapeAttr=e.escapeHtml,e.ensureHead=()=>{if(document.head)return document.head;const e=document.createElement("head");return document.documentElement.prepend(e),e},e.ensureBody=()=>{if(document.body){return"static"===getComputedStyle(document.body).position&&(document.body.style.position="relative"),document.body}const e=document.createElement("body");return e.style.position="relative",document.documentElement.appendChild(e),e},e.removeById=e=>{document.getElementById(e)?.remove()},e.isInternalElement=e=>!!(e&&e instanceof Element)&&Boolean(e.closest(`#${t.report}, #a11y-email-console-ui, #${t.overlay}, #${t.imageOverlay}, #${t.listOverlay}, #${t.headingsOverlay}`)),e.isTrackingPixel=e=>{if(!(e instanceof HTMLImageElement))return!1;const t=e.getBoundingClientRect(),a=Math.round(t.width),i=Math.round(t.height);if(a<=1&&i<=1||e.width<=1&&e.height<=1)return!0;const n=(e.getAttribute("src")||"").toLowerCase(),o=(e.getAttribute("alt")||"").trim();return(n.includes("tracking")||n.includes("openrate")||n.includes("pixel"))&&!o},e.isMeaningfulVisibleContent=t=>{if(!(t instanceof Element))return!1;if(e.isInternalElement(t)||!e.visible(t))return!1;if(t.matches("script,style,meta,link,noscript,template"))return!1;if(t.matches("img")&&e.isTrackingPixel(t))return!1;if(0===t.children.length){const e=(t.textContent||"").replace(/\s+/g," ").trim();return Boolean(e)||t.matches("img,svg,button,input,select,textarea,a,video,audio,iframe")}return!1},e.visible=t=>{if(!(t&&t instanceof Element))return!1;if(e.isInternalElement(t))return!1;const a=t;if("function"==typeof a.checkVisibility)try{return a.checkVisibility()}catch{}const i=getComputedStyle(t);if("none"===i.display||"hidden"===i.visibility||"0"===i.opacity)return!1;const n=t.getBoundingClientRect();return n.width>0&&n.height>0},e.getDirectText=e=>Array.from(e.childNodes).filter(e=>e.nodeType===Node.TEXT_NODE).map(e=>String(e.textContent||"").replace(/\s+/g," ").trim()).filter(Boolean).join(" ").trim(),e.getFontSize=e=>{const t=parseFloat(getComputedStyle(e).fontSize);return Number.isFinite(t)?t:0},e.getAccessibleName=t=>{if(!(t instanceof Element))return"";if(e.isInternalElement(t))return"";const a=t.getAttribute("aria-label");if(a&&a.trim())return a.trim();const i=t.getAttribute("aria-labelledby");if(i){const e=i.split(/\s+/).map(e=>document.getElementById(e)?.textContent||"").map(e=>e.replace(/\s+/g," ").trim()).filter(Boolean);if(e.length)return e.join(" ").trim()}const n=t.getAttribute("title");if(n&&n.trim())return n.trim();const o=(t.textContent||"").replace(/\s+/g," ").trim();if(o)return o;const r=t.querySelector?.("img[alt]");return(r?(r.getAttribute("alt")||"").trim():"")||""},e.makeSection=(t,a)=>`<section class="a11y-section"><h2>${e.escapeHtml(t)}</h2>${a}</section>`,e.annotationText=(e,t)=>`${e}: "${t}"`,e.clearHeadingAnnotations=()=>{document.querySelectorAll("[data-a11y-heading]").forEach(e=>{e.removeAttribute("data-a11y-heading"),e.removeAttribute("data-a11y-heading-level"),e.removeAttribute("data-a11y-heading-order"),e.removeAttribute("data-a11y-heading-label"),e.removeAttribute("data-a11y-heading-kind"),e.removeAttribute("data-text"),e.classList.remove("a11y-heading-highlight","a11y-heading-issue","a11y-heading-alt")})},e.clearLargeTextAnnotations=()=>{document.querySelectorAll("[data-a11y-large-text]").forEach(e=>{e.removeAttribute("data-a11y-large-text"),e.removeAttribute("data-a11y-large-text-label"),e.removeAttribute("data-a11y-large-text-size"),e.classList.remove("a11y-large-text-highlight","a11y-large-text-issue")})},e.clearTableAnnotations=()=>{document.querySelectorAll("[data-a11y-table]").forEach(e=>{e.removeAttribute("data-a11y-table"),e.removeAttribute("data-a11y-table-label"),e.removeAttribute("data-a11y-table-status"),e.classList.remove("a11y-table-highlight","a11y-table-issue","a11y-table-possible-list")}),document.querySelectorAll("[data-a11y-table-row-issue], [data-a11y-table-body-issue]").forEach(e=>{e.removeAttribute("data-a11y-table-row-issue"),e.removeAttribute("data-a11y-table-body-issue"),e.classList.remove("a11y-table-row-issue","a11y-table-body-issue"),e instanceof HTMLElement&&(e.style.outline="",e.style.outlineOffset="")})},e.clearImageAnnotations=()=>{document.querySelectorAll("[data-a11y-image]").forEach(e=>{e.removeAttribute("data-a11y-image"),e.removeAttribute("data-a11y-image-label"),e.removeAttribute("data-a11y-image-status"),e.classList.remove("a11y-image-present","a11y-image-empty","a11y-image-missing"),e instanceof HTMLElement&&(e.style.outline="",e.style.outlineOffset="")}),e.removeById(t.imageOverlay),e.removeById(t.overlay)},e.clearLinkAnnotations=()=>{document.querySelectorAll("[data-textlink], [data-a11y-link]").forEach(e=>{e.removeAttribute("data-textlink"),e.removeAttribute("data-a11y-link"),e.removeAttribute("data-a11y-link-label"),e.classList.remove("a11y-link-highlight","a11y-link-issue","a11y-link-ok","a11y-link-focus","a11y-link-image-label","a11y-link-image-missing","a11y-link-image-wrap"),e instanceof HTMLElement&&(e.style.outline="",e.style.outlineOffset="",e.style.display=""),e.querySelectorAll?.("[data-a11y-link-image]").forEach(e=>{e.removeAttribute("data-a11y-link-image"),e instanceof HTMLElement&&(e.style.outline="",e.style.outlineOffset="")})})},e.clearAll=()=>{e.removeById(t.report),e.removeById(t.styles),e.removeById(t.svg),e.removeById(t.imageOverlay),e.removeById(t.overlay),e.removeById(t.listOverlay),e.removeById(t.headingsOverlay),e.clearHeadingAnnotations(),e.clearLargeTextAnnotations(),e.clearTableAnnotations(),document.body?.classList.remove("a11y-table-linearise"),e.clearImageAnnotations(),e.clearLinkAnnotations(),document.querySelectorAll("[data-a11y-annotate], [data-text], [data-textlink], [data-a11y-list], [data-a11y-tabable], [data-a11y-tabstop-handlers], [data-a11y-link-image]").forEach(e=>{e.removeAttribute("data-a11y-annotate"),e.removeAttribute("data-text"),e.removeAttribute("data-textlink"),e.removeAttribute("data-a11y-list"),e.removeAttribute("data-a11y-tabable"),e.removeAttribute("data-a11y-list-possible"),e.removeAttribute("data-a11y-table-row-issue"),e.removeAttribute("data-a11y-table-body-issue"),e.removeAttribute("data-a11y-tabstop-handlers"),e.classList.remove("-js-tabable","a11y-heading-highlight","a11y-heading-issue","a11y-heading-alt","a11y-large-text-highlight","a11y-large-text-issue","a11y-table-highlight","a11y-table-issue","a11y-table-possible-list","a11y-table-row-issue","a11y-table-body-issue","a11y-image-present","a11y-image-empty","a11y-image-missing","a11y-link-highlight","a11y-link-issue","a11y-link-ok","a11y-link-focus","a11y-link-image-label","a11y-link-image-missing","a11y-link-image-wrap","a11y-possible-list-highlight"),e instanceof HTMLElement&&(e.style.outline="",e.style.outlineOffset="")}),e.clearTextSpacing?.()},e.setTableLinearise=t=>{const a=e.ensureBody();document.querySelectorAll(".a11y-preserve-hidden").forEach(e=>e.classList.remove("a11y-preserve-hidden")),document.querySelectorAll("table[data-a11y-linearise-width]").forEach(e=>{e.style.removeProperty("--a11y-linearise-width"),e.removeAttribute("data-a11y-linearise-width")}),t&&(document.querySelectorAll("table").forEach(e=>{const t=Math.round(e.getBoundingClientRect().width);t>0&&(e.setAttribute("data-a11y-linearise-width",String(t)),e.style.setProperty("--a11y-linearise-width",`${t}px`))}),document.querySelectorAll("table, table *").forEach(e=>{try{const t=getComputedStyle(e);("none"===t.display||"hidden"===t.visibility||e.hidden||"true"===e.getAttribute("aria-hidden"))&&e.classList.add("a11y-preserve-hidden")}catch{}})),a.classList.toggle("a11y-table-linearise",Boolean(t))},e.reportShellCss="\n#a11y-email-report{font:400 14px/1.5 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#222;background:transparent;border:0;margin:0;padding:0;display:block;max-width:100%;box-sizing:border-box}\n#a11y-email-report h2{font:600 18px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:0 0 8px}\n#a11y-email-report h3{font:600 15px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:12px 0 6px}\n#a11y-email-report ol,#a11y-email-report ul{margin:8px 0 0 20px;padding:0}\n#a11y-email-report li{margin:4px 0}\n#a11y-email-report code,#a11y-email-report kbd{background:#fff;border:1px solid #ccc;border-radius:4px;padding:0 .3em}\n#a11y-email-report .warn{color:#c00000;font-weight:600}\n#a11y-email-report .ok{color:#087f23;font-weight:600}\n#a11y-email-report .muted{color:#555}\n#a11y-email-report table{border-collapse:collapse;width:100%}\n#a11y-email-report td,#a11y-email-report th{border:1px solid #ddd;padding:4px 6px;vertical-align:top}\n#a11y-email-report .a11y-section{margin-bottom:16px}\n#a11y-email-report .a11y-annotation-prefix{font-weight:400}\n#a11y-email-report .a11y-annotation-quote{font-style:italic}\n#a11y-email-report .a11y-annotation-note{color:#b00020;font-weight:600}\n#a11y-email-report .a11y-compact-list{margin:8px 0 0 0;padding:0 !important;list-style:none}\n#a11y-email-report .a11y-compact-list li{margin:4px 0}\n#a11y-email-report .a11y-report-linkname{color:#0b5e20 !important;text-decoration:underline !important;font-weight:400 !important}\n#a11y-email-report .a11y-status-pill{display:inline-flex;align-items:center;min-width:2.2em;padding:2px 8px;border-radius:999px;background:#555;color:#fff;font:700 11px/1 system-ui;white-space:nowrap}\n#a11y-email-report .a11y-status-pill.ok{background:#087f23}\n#a11y-email-report .a11y-status-pill.warn{background:#b00020}\n#a11y-email-report .a11y-status-pill.info{background:#0057d9;color:#fff}\n#a11y-email-report .a11y-large-size-pill{background:#0057d9 !important;color:#fff !important}\n#a11y-email-report .a11y-large-prompt{color:#0057d9;font-weight:500}\n#a11y-email-report .a11y-report-linkname{color:#0b5e20 !important;text-decoration:underline !important;font-weight:400 !important}\n.a11y-inline-badge{position:absolute;inset:0 auto auto 0;z-index:10;pointer-events:none;display:inline-flex;align-items:flex-start;justify-content:flex-start;gap:4px;background:#0057d9;color:#fff;padding:2px 6px;font:600 12px/1.2 system-ui;border-radius:3px;box-shadow:0 0 0 1px rgba(0,0,0,.15);max-width:calc(100vw - 24px);white-space:nowrap}\n.a11y-highlight{outline:3px dotted #0057d9 !important;outline-offset:2px !important;position:relative}\n.a11y-highlight-ok{outline:3px solid #087f23 !important;outline-offset:2px !important;position:relative}\n.a11y-focus{outline:3px dashed #0057d9 !important;outline-offset:2px !important}\n#a11y-email-tabstops-svg{position:absolute;inset:0;overflow:visible;z-index:10000;pointer-events:none;margin:0}\n#a11y-email-tabstops-svg .line{stroke:#0057d9;stroke-width:4px}\n#a11y-email-tabstops-svg rect{fill:#0057d9;stroke:#0057d9;stroke-width:1px}\n#a11y-email-tabstops-svg text{fill:#fff;font-size:10px;font-weight:700}\n#a11y-email-tabstops-svg rect.-js-focused{fill:#0057d9 !important}\n.a11y-textspacing, .a11y-textspacing *{line-height:1.5 !important;letter-spacing:0.12em !important;word-spacing:0.16em !important}\nfont-family:inherit !important;font-size:inherit !important;font-weight:inherit;font-style:inherit}\n.a11y-textspacing p,.a11y-textspacing li,.a11y-textspacing td,.a11y-textspacing th,.a11y-textspacing a,.a11y-textspacing span,.a11y-textspacing div{word-break:normal;overflow-wrap:anywhere}\n.a11y-table-linearise table{display:block !important;width:var(--a11y-linearise-width, auto) !important;max-width:100% !important;box-sizing:border-box}.a11y-table-linearise tbody,.a11y-table-linearise tr,.a11y-table-linearise table,.a11y-table-linearise td,.a11y-table-linearise th{display:block !important;width:100% !important;box-sizing:border-box;text-align:left !important;clear:both !important}\n.a11y-table-linearise .a11y-preserve-hidden{display:none !important}\n.a11y-table-linearise [hidden],.a11y-table-linearise [aria-hidden=\"true\"]{display:none !important}\n.a11y-table-linearise td,.a11y-table-linearise th{border-left:0 !important;border-right:0 !important}\n.a11y-table-linearise table{margin-bottom:12px}\n.a11y-table-linearise tr{padding:4px 0}\n.a11y-table-linearise td,.a11y-table-linearise th{padding:4px 6px}\n.a11y-heading-highlight{position:relative;outline-offset:2px !important;outline:3px solid #087f23 !important}\n.a11y-heading-highlight::after{content:attr(data-a11y-heading-label);position:absolute;top:0;right:0;transform:translate(4px,-50%);background:#087f23;color:#fff;padding:2px 6px;border-radius:999px;font:700 11px/1 system-ui;box-shadow:0 0 0 1px rgba(0,0,0,.15);pointer-events:none;z-index:2;white-space:nowrap}\n.a11y-heading-issue{outline-style:dashed !important;outline-color:#c00000 !important}\n.a11y-heading-issue::after{background:#c00000}\n.a11y-heading-alt{outline-style:solid !important;outline-color:#087f23 !important}\n.a11y-heading-flag{display:inline-flex;align-items:center;justify-content:center;min-width:2.4em;padding:2px 6px;border-radius:999px;background:#087f23;color:#fff;font:700 11px/1 system-ui;white-space:nowrap;flex:none}\n.a11y-heading-row{display:flex;align-items:flex-start;gap:8px;margin:0 0 6px}\n.a11y-heading-copy{min-width:0;flex:1}\n.a11y-heading-issue-text{color:#b00020;font-weight:600}\n.a11y-large-text-highlight{position:relative;outline:3px dashed #0057d9 !important;outline-offset:2px !important}\n.a11y-large-text-highlight::before{content:attr(data-a11y-large-text-label);position:absolute;top:0;right:0;transform:translate(4px,-50%);background:#0057d9;color:#fff;padding:2px 6px;border-radius:999px;font:700 11px/1 system-ui;box-shadow:0 0 0 1px rgba(0,0,0,.15);pointer-events:none;z-index:2;white-space:nowrap}\n.a11y-large-text-issue{outline-style:dashed !important;outline-color:#0057d9 !important}\n.a11y-table-highlight{position:relative;outline:1px solid #087f23 !important;outline-offset:2px !important}\n.a11y-table-highlight::before{content:attr(data-a11y-table-label);position:absolute;top:0;right:0;transform:translate(4px,-50%);background:#087f23;color:#fff;padding:2px 6px;border-radius:999px;font:700 11px/1 system-ui;box-shadow:0 0 0 1px rgba(0,0,0,.15);pointer-events:none;z-index:2;white-space:nowrap}\n.a11y-table-issue{outline-style:dotted !important;outline-color:#c00000 !important}\n.a11y-table-possible-list{outline:3px dashed #0057d9 !important;outline-offset:2px !important}\n.a11y-table-row-issue, .a11y-table-body-issue{outline:2px dotted #c00000 !important;outline-offset:2px !important}\n.a11y-image-present{outline:3px solid #087f23 !important;outline-offset:2px !important}\n.a11y-image-empty{outline:3px dashed #0057d9 !important;outline-offset:2px !important}\n.a11y-image-missing{outline:3px dotted #c00000 !important;outline-offset:2px !important}\n.a11y-link-ok{outline:4px solid #087f23 !important;outline-offset:2px !important}\n.a11y-link-focus{outline:4px dashed #0057d9 !important;outline-offset:2px !important}\n.a11y-link-issue{outline:4px dotted #c00000 !important;outline-offset:2px !important;display:inline-block;position:relative}\n.a11y-link-image-wrap{display:inline-block;position:relative}\n.a11y-link-ok img,.a11y-link-highlight img{outline:2px solid #087f23 !important;outline-offset:2px !important}\n.a11y-link-issue img{outline:2px dotted #c00000 !important;outline-offset:2px !important}\n.a11y-link-focus img{outline:2px dashed #0057d9 !important;outline-offset:2px !important}\n.a11y-link-image-label{position:relative;display:inline-block}\n.a11y-link-image-label::before{box-sizing:border-box;content:attr(data-a11y-link-label);color:#fff;background-color:#0057d9;text-shadow:0 0 1px #000;z-index:2;align-items:flex-start;justify-content:flex-start;width:max-content;min-height:100%;padding:2px 6px;font:500 14px/1.3 sans-serif;display:flex;position:absolute;inset:0 0 auto auto;transform:translate(4px,-50%)}\n[data-textlink]{--a-outline-color:#c00000;--a-outline-width:4px;--a-outline-style:dotted;--a-outline-offset:2px;display:inline-block;position:relative}\n[data-textlink]:after{box-sizing:border-box;content:attr(data-textlink);color:#fff;background-color:#c00000;text-shadow:0 0 1px #000;z-index:1;align-items:center;width:max-content;min-height:100%;margin-left:8px;padding:2px 6px;font:500 14px/1.3 sans-serif;display:flex;position:absolute;inset:0 -100% auto 100%}\n.a11y-possible-list-highlight{outline:3px dashed #0057d9 !important;outline-offset:2px !important}\n.a11y-possible-list-highlight::before{content:'Possible visual list';position:absolute;top:0;right:0;transform:translate(4px,-50%);background:#0057d9;color:#fff;padding:2px 6px;border-radius:999px;font:700 11px/1 system-ui;box-shadow:0 0 0 1px rgba(0,0,0,.15);pointer-events:none;z-index:2;white-space:nowrap}\n.a11y-tabindex-issue{outline:2px dotted #c00000 !important;outline-offset:2px !important;position:relative}\n[data-text]:not(.a11y-heading-highlight):not(.a11y-image-empty){display:inline-block;position:relative;outline:2px dotted #c00000 !important;outline-offset:2px !important}\n[data-text]:not(.a11y-heading-highlight):not(.a11y-image-empty)::after{box-sizing:border-box;content:attr(data-text);color:#fff;background-color:#c00000;text-shadow:0 0 1px #000;z-index:1;align-items:center;width:max-content;min-height:100%;margin-left:8px;padding:2px 6px;font:500 14px/1.3 sans-serif;display:flex;position:absolute;inset:0 -100% auto 100%}\n.a11y-image-empty[data-text]{outline:2px dashed #0057d9 !important;outline-offset:2px !important;position:relative}\n.a11y-image-empty[data-text]::after{background-color:#0057d9 !important}\n "})(),(()=>{const e=window.A11Y_EMAIL;if(e.injectStyles=t=>{const a=e.ids.styles;let i=document.getElementById(a);return i||(i=document.createElement("style"),i.id=a,i.textContent=t,e.ensureHead().appendChild(i),i)},e.toggleReport=t=>{let a=document.getElementById(e.ids.report);if(!a){a=document.createElement("output"),a.id=e.ids.report,a.className="a11y-report",a.setAttribute("role","region"),a.setAttribute("aria-label","Accessibility report");const t=document.getElementById("a11y-email-console-ui");t?t.appendChild(a):e.ensureBody().prepend(a)}return a.innerHTML=t,a},e.ensureGeometryOverlay=()=>{let t=document.getElementById(e.ids.overlay);return t||(t=document.createElement("div"),t.id=e.ids.overlay,t.setAttribute("aria-hidden","true"),Object.assign(t.style,{position:"fixed",inset:"0",pointerEvents:"none",zIndex:"2147483646",overflow:"visible"}),document.documentElement.appendChild(t),t)},e.makeGeometryBadge=(e,t="blue")=>{const a=document.createElement("span");return a.className=`a11y-geometry-badge a11y-theme-${t}`,a.textContent=e,a},e.positionGeometryBadge=(e,t,a="top-right",i=4,n=0)=>{const o=t.getBoundingClientRect();let r=o.right+i,l=o.top+n;"top-left"===a?(r=o.left+i,l=o.top+n):"top-right-inside"===a?(r=Math.max(0,o.right-i),l=o.top+n,e.style.transform="translate(-100%, 0)"):"mid-right"===a&&(r=o.right+i,l=o.top+o.height/2+n,e.style.transform="translate(0, -50%)"),e.style.left=`${Math.round(r)}px`,e.style.top=`${Math.round(l)}px`},e.renderGeometryOverlay=()=>{e.removeById(e.ids.overlay);const t=e.ensureGeometryOverlay(),a=new WeakMap,i=(i,n,o,r="top-right",l=4,s=0)=>{if(!(i instanceof Element&&e.visible(i)&&n))return;const d=a.get(i)||new Set,c=`${o}|${r}|${n}`;if(d.has(c))return;d.add(c),a.set(i,d);const p=e.makeGeometryBadge(n,o);e.positionGeometryBadge(p,i,r,l,s),t.appendChild(p)};Array.from(document.querySelectorAll('[data-a11y-heading="true"]')).forEach(e=>{const t=e.classList.contains("a11y-heading-issue");i(e,e.dataset.a11yHeadingLabel||"H",t?"red":"green","top-right-inside",2,0),e.dataset.text&&i(e,e.dataset.text,"red","mid-right",8,0)}),Array.from(document.querySelectorAll('[data-a11y-large-text="true"]')).forEach(e=>{i(e,e.dataset.a11yLargeTextLabel||"","blue","top-right-inside",2,0)}),Array.from(document.querySelectorAll('[data-a11y-link="true"]')).forEach(e=>{const t=e.dataset.a11yLinkLabel||"",a=e.getAttribute("data-textlink")||"";t&&i(e,t,e.classList.contains("a11y-link-image-missing")?"red":"green","top-right",4,0),a&&i(e,a,"red","mid-right",8,0)}),Array.from(document.querySelectorAll('[data-a11y-image="true"]')).forEach(e=>{const t=e.dataset.a11yImageStatus||"present",a=e.dataset.a11yImageLabel||"";a&&i(e,a,"missing"===t?"red":"empty"===t?"blue":"green","top-left",0,0)}),Array.from(document.querySelectorAll('[data-a11y-table="true"]')).forEach(e=>{const t=e.dataset.a11yTableLabel||"",a="issue"===e.dataset.a11yTableStatus?"red":e.classList.contains("a11y-table-possible-list")?"blue":"green";t&&i(e,t,a,"top-right-inside",2,0),e.dataset.text&&i(e,e.dataset.text,"red","mid-right",8,0)}),Array.from(document.querySelectorAll("[data-text]:not([data-a11y-image]):not([data-a11y-table]):not([data-a11y-heading])")).forEach(e=>{i(e,e.getAttribute("data-text")||"",e.classList.contains("a11y-image-empty")?"blue":"red","mid-right",8,0)})},!window.__a11yGeometryOverlayHandlers){const t=()=>{document.getElementById(e.ids.overlay)&&e.renderGeometryOverlay()};window.addEventListener("scroll",t,!0),window.addEventListener("resize",t,!0),window.__a11yGeometryOverlayHandlers=!0}e.reportShellCss='\n#a11y-email-report{font:400 14px/1.5 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#222;background:transparent;border:0;margin:0;padding:0;display:block;max-width:100%;box-sizing:border-box}\n#a11y-email-report h2{font:600 18px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:0 0 8px}\n#a11y-email-report h3{font:600 15px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:12px 0 6px}\n#a11y-email-report ol,#a11y-email-report ul{margin:8px 0 0 20px;padding:0}\n#a11y-email-report li{margin:4px 0}\n#a11y-email-report code,#a11y-email-report kbd{background:#fff;border:1px solid #ccc;border-radius:4px;padding:0 .3em}\n#a11y-email-report .warn{color:#c00000;font-weight:600}\n#a11y-email-report .ok{color:#087f23;font-weight:600}\n#a11y-email-report .muted{color:#555}\n#a11y-email-report .a11y-status-pill{display:inline-flex;align-items:center;justify-content:center;min-width:2.4em;padding:2px 8px;border-radius:999px;background:#666;color:#fff;font:700 12px/1 system-ui;white-space:nowrap;flex:none}\n#a11y-email-report .a11y-status-pill.info,#a11y-email-report .a11y-large-size-pill{background:#0057d9 !important;color:#fff !important}\n#a11y-email-report .a11y-large-prompt{color:#0057d9;font-weight:500}\n#a11y-email-report .a11y-report-linkname{color:#0b5e20 !important;text-decoration:underline !important;font-weight:400 !important}\n.a11y-section{margin:0 0 16px}\n.a11y-heading-row{display:flex;align-items:flex-start;gap:8px}\n.a11y-heading-flag{display:inline-flex;align-items:center;justify-content:center;min-width:32px;padding:2px 8px;border-radius:999px;background:#087f23;color:#fff;font:700 12px/1 system-ui}\n.a11y-heading-copy{display:inline-block}\n.a11y-annotation-prefix{font-weight:400}\n.a11y-annotation-quote{font-weight:700}\n.a11y-annotation-note{color:#c00000;font-weight:700}\n.a11y-compact-list{margin-left:20px}\n.a11y-geometry-badge{position:fixed;pointer-events:none;display:inline-flex;align-items:flex-start;justify-content:flex-start;padding:2px 6px;font:600 12px/1.2 system-ui;border-radius:3px;box-shadow:0 0 0 1px rgba(0,0,0,.15);max-width:min(360px, calc(100vw - 16px));white-space:normal;word-break:break-word}\n.a11y-theme-blue{background:#0057d9;color:#fff}\n.a11y-theme-red{background:#c00000;color:#fff}\n.a11y-theme-green{background:#087f23;color:#fff}\n.a11y-heading-highlight{outline:2px solid #087f23 !important;outline-offset:2px !important}\n.a11y-heading-issue{outline:2px dotted #c00000 !important}\n.a11y-large-text-highlight{outline:2px dashed #0057d9 !important;outline-offset:2px !important}\n.a11y-table-highlight{outline:1px solid #b7efc5 !important;outline-offset:2px !important}\n.a11y-table-issue{outline:2px dotted #c00000 !important}\n.a11y-table-possible-list{outline:2px dashed #0057d9 !important}\n.a11y-link-highlight{outline:2px solid #087f23 !important;outline-offset:2px !important}\n.a11y-link-issue{outline:2px dotted #c00000 !important}\n.a11y-link-focus{outline:2px dashed #0057d9 !important}\n.a11y-image-present{outline:2px solid #087f23 !important;outline-offset:2px !important}\n.a11y-image-empty{outline:2px dashed #0057d9 !important;outline-offset:2px !important}\n.a11y-image-missing{outline:2px dotted #c00000 !important;outline-offset:2px !important}\n#a11y-email-tabstops-svg{--bg-color:yellow;--focus-color:#0057d9}\n#a11y-email-tabstops-svg .line{stroke:var(--bg-color);stroke-width:4px}\n#a11y-email-tabstops-svg rect{fill:var(--bg-color);stroke:#000;stroke-width:1px}\n#a11y-email-tabstops-svg text{fill:#000;font-size:10px;font-weight:700}\n#a11y-email-tabstops-svg rect.-js-focused{fill:var(--focus-color) !important}\nbody.a11y-table-linearise table{width:var(--a11y-linearise-width, auto) !important;max-width:100% !important;box-sizing:border-box}body.a11y-table-linearise table:not([role=main]){width:var(--a11y-linearise-width, auto) !important;max-width:var(--a11y-linearise-width, auto) !important;margin-left:auto !important;margin-right:auto !important;box-sizing:border-box}body.a11y-table-linearise table:not([role=main]),body.a11y-table-linearise table:not([role=main]) tbody,body.a11y-table-linearise table:not([role=main]) thead,body.a11y-table-linearise table:not([role=main]) tfoot,body.a11y-table-linearise table:not([role=main]) tr{display:block !important;width:100% !important;box-sizing:border-box}body.a11y-table-linearise table:not([role=main]) :is(table,th,td){display:block !important;text-align:left !important;outline:1px solid #0f0 !important;clear:both !important;margin-left:0 !important;padding-left:0 !important;width:100% !important;box-sizing:border-box}body.a11y-table-linearise .a11y-preserve-hidden{display:none !important}\nbody.a11y-table-linearise table:not([role=main]) tr{margin:0 0 8px !important}\nbody.a11y-table-linearise [hidden],body.a11y-table-linearise [aria-hidden="true"],body.a11y-table-linearise [style*="display:none"],body.a11y-table-linearise [style*="display: none"]{display:none !important}\n'})(),(()=>{const e=window.A11Y_EMAIL,t=e=>{const t=document.createElement("textarea");return t.innerHTML=String(e||""),t.value},a=e=>{const a=String(e||"").match(/\balt\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i);if(!a)return"missing";return""===t(null!=a[1]?a[1]:null!=a[2]?a[2]:a[3]||"")?"empty":"present"},i=()=>{const e=(e=>t(String(e||"")).match(/<v:roundrect\b[^>]*>/gi)||[])((()=>{try{return document.documentElement&&document.documentElement.outerHTML||""}catch(e){return""}})());if(!e.length)return{foundRoundrect:!1,missingAlt:!1,tagsChecked:0};let i=!1;for(const t of e){"empty"!==a(t)&&(i=!0)}return{foundRoundrect:!0,missingAlt:i,tagsChecked:e.length}},n=e=>{const t=String(e||"").trim().toLowerCase();return"santander"===t||"santander email"===t};e.checkEmailMeta=()=>{const t=[],a=Array.from(document.querySelectorAll('main, [role="main"]')).filter(e.visible),o=a.find(e=>"main"===(e.getAttribute("role")||"").toLowerCase()&&"ltr"===(e.getAttribute("dir")||"").toLowerCase()&&n(e.getAttribute("aria-label"))),r=(()=>{const t=Array.from(document.querySelectorAll("table")).filter(e.visible);return t.length?t.find(e=>!t.some(t=>t!==e&&t.contains(e)))||t[0]:null})(),l=o||r||a[0]||document.body,s=Array.from(document.body.querySelectorAll("*")).filter(e.isMeaningfulVisibleContent).filter(t=>{if(t.closest(`#${e.ids.panel}`))return!1;if(t instanceof HTMLImageElement){const e=Number(t.getAttribute("width")||0),a=Number(t.getAttribute("height")||0);if(e<=1&&a<=1||/tracking|open/i.test(t.getAttribute("alt")||""))return!1}return!0}).filter(e=>e!==l&&!l.contains(e)),d=[["role","main"===(l.getAttribute("role")||"").toLowerCase(),'Expected role="main" on a single enclosing content container'],["dir","ltr"===(l.getAttribute("dir")||"").toLowerCase(),'Expected dir="ltr" on the same content container'],["aria-label",n(l.getAttribute("aria-label")),'Expected aria-label="Santander" or aria-label="Santander email" on the same content container'],["encloses",0===s.length,0===s.length?"OK":"Expected visible email content to sit within that same single content container"],["lang","en-gb"===(l.getAttribute("lang")||"").toLowerCase(),'Suggestion: add lang="en-gb" to the same content container']];for(const[e,a,i]of d)a||"lang"===e||t.push({key:e,issue:i,el:l});const c=i(),p=c.foundRoundrect&&c.missingAlt;p&&t.push({key:"outlook-roundrect",issue:"Email contains a hidden Outlook v:roundrect image component which is missing an alt text. Please report to the developers to repair.",el:l});return{findings:t,html:e.makeSection("Email container rules",`\n <p>Checks for a single enclosing HTML email container.</p>\n <ul>\n ${d.map(([,t,a])=>`<li>${t?'<span class="ok">OK</span>':'<span class="warn">Issue</span>'} - ${e.escapeHtml(a)}</li>`).join("")}\n <li>${p?'<span class="warn">Issue</span> - Email contains a hidden Outlook v:roundrect image component which is missing an alt text. Please report to the developers to repair.':'<span class="ok">OK</span> - No hidden Outlook v:roundrect image component issue found.'}</li>\n </ul>\n <p class="muted">- Container inspected: <code>${e.escapeHtml(l.tagName.toLowerCase())}</code></p>\n `)}}})(),(()=>{const e=window.A11Y_EMAIL,t=t=>e.getDirectText(t),a=(a,i)=>{if(!(a instanceof HTMLElement))return!1;if(!e.visible(a))return!1;if(a.closest("a[href]"))return!1;if(a.closest(':is(h1,h2,h3,h4,h5,h6,[role="heading"])'))return!1;if(a.closest(`#${e.ids.report}, #${e.ids.svg}, #${e.ids.imageOverlay}`))return!1;return!!t(a)&&e.getFontSize(a)>i+.1};e.checkLargeText=(i=16)=>{e.clearLargeTextAnnotations();const n=Array.from(document.querySelectorAll("body *")).filter(e=>a(e,i)),o=[];for(const r of n){const n=e.getFontSize(r),l=r.parentElement;if(l&&l instanceof HTMLElement&&a(l,i)&&e.getFontSize(l)>=n)continue;const s=t(r);s&&(r.dataset.a11yLargeText="true",r.dataset.a11yLargeTextLabel=`${Math.round(n)}px`,r.dataset.a11yLargeTextSize=String(Math.round(n)),r.classList.add("a11y-large-text-highlight"),o.push({el:r,text:s,fontSize:Math.round(n),issue:`Text is larger than the base font size (${i}px) and is not marked as a heading`}))}return{findings:o,html:e.makeSection("Large text",`\n <p>Please review the large text outlined in dashed blue and consider whether it should be marked up as a heading.</p>\n <p>A heading should introduce the content that follows, so it’s perfectly fine for some text to remain styled as large text rather than being treated as a heading.</p>\n <p>${o.length?"Large text detected.":"No large text detected."}</p>\n `)}}})(),(()=>{const e=window.A11Y_EMAIL,t=e=>{const t=e.getAttribute("aria-level");if(t){const e=parseInt(t,10);if(!Number.isNaN(e)&&e>=1&&e<=6)return e}const a=e.tagName.match(/^H([1-6])$/i);return a?parseInt(a[1],10):6},a=e=>{const t=(e.textContent||"").trim(),a=e.querySelector("img"),i=a?a.getAttribute("alt"):null;return{text:t,img:a,alt:null===i?null:i.trim(),altAttr:i}};e.checkHeadings=(i={})=>{const n=Array.from(document.querySelectorAll(':is(h1,h2,h3,h4,h5,h6,[role="heading"])')).filter(e.visible),o=[],r=Array.isArray(i.largeTextFindings)?i.largeTextFindings:[],l=n.filter(e=>1===t(e));let s=0,d=0,c=0;e.clearHeadingAnnotations();for(const e of n){const i=t(e),{text:n,img:r,alt:p,altAttr:m}=a(e),u=Boolean(r&&!n&&p),g=Boolean(r&&!n&&null===m),y=Boolean(r&&!n&&null!==m&&""===p);let h="";const b=`H${i}`;let f=n;c+=1,e.dataset.a11yHeading="true",e.dataset.a11yHeadingLevel=String(i),e.dataset.a11yHeadingOrder=String(c),e.dataset.a11yHeadingLabel=b,e.classList.add("a11y-heading-highlight"),u?(f=p,e.dataset.a11yHeadingAlt="true",e.dataset.a11yHeadingKind="alt",e.classList.add("a11y-heading-alt")):g?h="MISSING alt text":y&&(h="Empty alt text"),n||u||r||(h=h||`Empty H${i} heading`),1===i&&(d+=1,l.length>1&&d>1&&(h=h?`${h} Only one H1 heading is allowed.`:"Only one H1 heading is allowed.")),s&&i>s+1&&(h=h?`${h} Should be a H${s+1}, or lower, but it's a H${i}`:`Should be a H${s+1}, or lower, but it's a H${i}`),h&&(e.setAttribute("data-text",h),e.classList.add("a11y-heading-issue")),o.push({level:i,text:f||(p??"")||"(empty)",issue:h,el:e,order:c,fromAlt:u,missingAlt:g,emptyAlt:y,type:"heading"}),s=i}document.querySelector('h1, [role="heading"][aria-level="1"]')||o.unshift({level:1,text:"MISSING H1",issue:"A H1 heading is a requirement",synthetic:!0,order:0,fromAlt:!1,type:"heading"});const p=[];for(const e of o)p.push(e);for(const e of r)p.push({...e,type:"largeText"});p.sort((e,t)=>e.synthetic&&!t.synthetic?-1:!e.synthetic&&t.synthetic?1:e.el&&t.el?((e,t)=>{if(!e||!t||e===t)return 0;const a=e.compareDocumentPosition(t);return a&Node.DOCUMENT_POSITION_FOLLOWING?-1:a&Node.DOCUMENT_POSITION_PRECEDING?1:0})(e.el,t.el):0);let m=1;const u=p.map(t=>{if("largeText"===t.type){return`<li class="a11y-heading-row a11y-large-row" style="margin-left:${Math.max(0,18*m)}px"><span class="a11y-status-pill info a11y-large-size-pill">${e.escapeHtml(String(t.fontSize))}px</span><span class="a11y-heading-copy a11y-large-copy">${e.escapeHtml(t.text)} <span class="a11y-large-prompt">- Is this a heading?</span></span></li>`}m=Number(t.level)||m;const a=Math.max(0,18*(m-1)),i=`<span class="a11y-heading-flag">${e.escapeHtml(`H${m}`)}</span>`;if(t.fromAlt){const n=t.issue?` <strong class="a11y-annotation-note">- ${e.escapeHtml(t.issue)}</strong>`:"";return`<li class="a11y-heading-row" style="margin-left:${a}px">${i}<span class="a11y-heading-copy"><span class="a11y-annotation-prefix">alt=</span> <strong class="a11y-annotation-quote">"${e.escapeHtml(t.text)}"</strong>${n}</span></li>`}if(t.emptyAlt)return`<li class="a11y-heading-row" style="margin-left:${a}px">${i}<span class="a11y-heading-copy"><strong class="a11y-annotation-note">- Empty alt text</strong></span></li>`;if(t.missingAlt)return`<li class="a11y-heading-row" style="margin-left:${a}px">${i}<span class="a11y-heading-copy"><strong class="a11y-annotation-note">- MISSING alt text</strong></span></li>`;const n=t.issue?` <strong class="a11y-annotation-note">- ${e.escapeHtml(t.issue)}</strong>`:"";return`<li class="a11y-heading-row" style="margin-left:${a}px">${i}<span class="a11y-heading-copy"><strong>${e.escapeHtml(t.text)}</strong>${n}</span></li>`}).join("");return{findings:o,html:e.makeSection("Headings and large text",`\n <p><strong>Check:</strong> heading structure, empty headings, H1 usage, heading order, headings made from image alt text, and non-heading large text.</p>\n <ol>\n <li>Is there a single main <code>H1</code> heading at the top of the page?</li>\n <li>Are headings arranged in a logical order from H1 to H6?</li>\n <li>Do all headings clearly describe the content that follows?</li>\n <li>Have all empty headings been removed?</li>\n <li>Please review the large text outlined in dashed blue and consider whether it should be marked up as a heading.</li>\n </ol>\n <p>Please review the large text outlined in dashed blue and consider whether it should be marked up as a heading.</p>\n <p>A heading should introduce the content that follows, so it’s perfectly fine for some text to remain styled as large text rather than being treated as a heading.</p>\n <ol class="a11y-compact-list">${u}</ol>\n `)}}})(),(()=>{const e=window.A11Y_EMAIL,t=e=>String(e||"").replace(/\s+/g," ").trim(),a=i=>{if(!i)return"";if(i.nodeType===Node.TEXT_NODE)return t(i.textContent||"");if(i.nodeType!==Node.ELEMENT_NODE)return"";const n=i;if(!e.visible(n))return"";if(n.matches("img,svg,script,style,noscript"))return"";const o=[];for(const e of Array.from(n.childNodes)){const t=a(e);t&&o.push(t)}return t(o.join(" "))},i=e=>{const i=e.querySelector("img"),n=a(e),o=i?i.getAttribute("alt"):null,r=null===o?null:t(o),l=Boolean(i),s=null!==r&&""!==r;return{img:i,visibleText:n,alt:r,hasImage:l,hasImageAlt:s,imageAltMissing:l&&!s,accessibleName:t(e.getAttribute("aria-label")||"")||n||(s?r:"")}},n=(e,a,i,n)=>{if(!i||!n)return!1;for(const o of e){if(o===a)continue;const e=t(o.getAttribute("href")||"");if(!e)continue;const r=t(o.dataset.a11yLinkName||"");if(r&&r===i&&e!==n)return!0}return!1},o=e=>{const a=t(e).toLowerCase();return["see all","see more","view all","view more","read more","more","click here","here","find out more"].includes(a)},r=e=>{const t=e.href||"";return!t.startsWith("javascript:")&&[".pdf",".xlsx",".docx",".pptx"].some(e=>t.toLowerCase().includes(e))},l=e=>{try{const t=new URL(e);t.search="",t.hash="";const a=t.pathname.toLowerCase().match(/\.([a-z0-9]+)$/i);return(a?.[1]||"").toLowerCase()}catch{return""}};e.checkLinks=()=>{e.clearLinkAnnotations();const a=Array.from(document.querySelectorAll("a")),s=[],d={missingHref:!1,duplicate:!1,imageAltMissing:!1};for(const c of a){if(!e.visible(c))continue;const{img:p,alt:m,hasImage:u,hasImageAlt:g,imageAltMissing:y,accessibleName:h}=i(c),b=t(c.getAttribute("href")||""),f=Boolean(c.closest("table")),x=[];if(c.dataset.a11yLink="true",c.dataset.a11yLinkName=h||"",c.classList.add("a11y-link-highlight"),c.style.outlineOffset="2px",u&&(c.dataset.a11yLinkLabel=g?m:"Link image without alt text",c.classList.add("a11y-link-image-label","a11y-link-image-wrap"),c.style.outlineOffset="2px",c.style.display="inline-block",c.style.outline="4px solid #087f23",p&&(p.setAttribute("data-a11y-link-image","true"),p.style.outline="2px solid #087f23",p.style.outlineOffset="2px",p.style.display="inline-block"),g||c.classList.add("a11y-link-image-missing")),b||(x.push("MISSING HREF"),d.missingHref=!0),u&&y&&(x.push("Link image without alt text"),d.imageAltMissing=!0),b&&!g&&h&&n(a,c,h,b)&&(x.push("Duplicated text with different destination"),d.duplicate=!0),b&&h&&o(h)&&x.push("Link text is too generic"),b&&!f&&"_blank"===(c.getAttribute("target")||"").toLowerCase()){"none"===getComputedStyle(c,"::after").content&&x.push("Link should clearly indicate that it opens in a new window or tab")}if(b&&r(c)){const e=l(c.href);e&&!t(h||"").toLowerCase().includes(e)&&x.push("Document link should include the file format and size")}const v=x.join(". ").trim();v?(c.setAttribute("data-textlink","MISSING HREF"===x[0]&&1===x.length?"MISSING HREF":v),c.classList.add("a11y-link-issue"),c.classList.remove("a11y-link-ok"),u&&(c.style.outline="4px dotted #c00000",p&&(p.style.outline="2px dotted #c00000"))):h&&(c.classList.add("a11y-link-ok"),c.style.outline="4px solid #087f23",u&&p&&(p.style.outline="2px solid #087f23")),c.dataset.a11yLinkHandlers||(c.dataset.a11yLinkHandlers="true",c.addEventListener("focus",()=>c.classList.add("a11y-link-focus")),c.addEventListener("blur",()=>c.classList.remove("a11y-link-focus")));const w=h||(u&&y?"Link image without alt text":"(empty)"),k=w;s.push({el:c,accessibleName:w,reportName:k,alt:m,href:b,issue:v,hasImage:Boolean(p),hasImageAlt:g,imageAltMissing:y,issueParts:x})}const c=e.makeSection("Links",`\n <ol>\n <li>Can the link text be understood on its own, without relying on the content around it?</li>\n <li>Does the link text avoid general terms like "Find out more" or "Click here"?</li>\n <li>Does the link text accurately describe the destination it goes to?</li>\n <li>Do links using the same text go to the same destination?</li>\n <li>Do text links use more than just colour, to be distinguishable?</li>\n </ol>\n <p>All links are listed below by accessible name.</p>\n <ol class="a11y-compact-list">\n ${s.map(t=>{const a=e.escapeHtml(t.reportName||t.accessibleName),i=Array.isArray(t.issueParts)&&t.issueParts.length?t.issueParts[0]:"";return`\n <li class="a11y-heading-row">\n <span class="a11y-heading-flag">LINK</span>\n <span class="a11y-heading-copy"><span class="a11y-report-linkname">${a}</span>${["MISSING HREF","Duplicated text with different destination","Link image without alt text"].includes(i)?` <strong class="warn">- ${e.escapeHtml(i)}</strong>`:""}</span>\n </li>\n `}).join("")}\n </ol>\n `);return{findings:s,html:c}}})(),(()=>{const e=window.A11Y_EMAIL;e.renderImageOverlay=()=>{},e.checkImages=()=>{const t=Array.from(document.querySelectorAll("img")).filter(e.visible),a=[];for(const e of t){const t=(e.getAttribute("role")||"").toLowerCase(),i=e.getAttribute("alt"),n=null===i?null:i.trim(),o="presentation"===t||"none"===t?"none":null===n?"missing":""===n?"empty":"present";e.setAttribute("data-a11y-image","true"),e.dataset.a11yImageStatus=o,e.classList.remove("a11y-image-present","a11y-image-empty","a11y-image-missing"),e.removeAttribute("data-text"),"missing"===o?(e.classList.add("a11y-image-missing"),e.setAttribute("data-text","MISSING ALT TEXT"),e.dataset.a11yImageLabel="MISSING ALT TEXT"):"empty"===o?(e.classList.add("a11y-image-empty"),e.setAttribute("data-text","Decorative"),e.dataset.a11yImageLabel="Decorative"):"none"===o?(e.classList.add("a11y-image-present"),e.setAttribute("data-text","None"),e.dataset.a11yImageLabel="None"):(e.classList.add("a11y-image-present"),e.dataset.a11yImageLabel=n),a.push({el:e,alt:n,status:o,decorative:"empty"===o||"none"===o,issue:"missing"===o?"Image alt-text is missing":""})}return{findings:a,html:e.makeSection("Images and alt text",'\n <p>Please manually check all image alt texts are appropriate, and that decorative images do not add any extra meaning to the content.</p>\n <ul>\n <li><span class="a11y-key-swatch a11y-key-swatch-green"></span> Outline solid green + alt text - check the alt text is relevant.</li>\n <li><span class="a11y-key-swatch a11y-key-swatch-blue"></span> Outline dashed blue + "Decorative" - Check image does not add any further meaning to the content.</li>\n <li><span class="a11y-key-swatch a11y-key-swatch-green"></span> Outline solid green + "None" - Image has no role and empty alt text.</li>\n <li><span class="a11y-key-swatch a11y-key-swatch-red"></span> Outline dotted red + "MISSING ALT TEXT" - Image requires an alt text.</li>\n </ul>\n ')}}})(),(()=>{const e=window.A11Y_EMAIL;e.checkTables=()=>{const t=Array.from(document.querySelectorAll("table")).filter(e.visible),a=[];e.clearTableAnnotations();for(const e of t){const t=(e.getAttribute("role")||"").toLowerCase(),i=e.hasAttribute("role");let n="ok",o="";e.dataset.a11yTable="true",e.classList.remove("a11y-table-highlight","a11y-table-issue","a11y-table-possible-list"),e.removeAttribute("data-text"),i?"presentation"===t||"none"===t?(n="ok",o="",e.classList.add("a11y-table-highlight")):(n="info",o=t||"role",e.classList.add("a11y-table-possible-list")):(n="issue",o="MISSING ROLE",e.classList.add("a11y-table-issue")),e.dataset.a11yTableStatus=n,o?e.dataset.a11yTableLabel=o:e.removeAttribute("data-a11y-table-label"),a.push({el:e,role:t,hasRole:i,status:n,hasIssues:"issue"===n})}const i=a.filter(e=>!e.hasRole).length,n=a.filter(e=>e.hasRole&&e.role&&"presentation"!==e.role&&"none"!==e.role),o=Array.from(new Set(n.map(e=>e.role))).join(", "),r=[];r.push("Tables with light green solid outline are marked correctly as presentational."),r.push("Tables with red dotted outline do not have a role stated."),r.push('It is expected for the outermost table to have a role of "main".'),i&&r.push("Table(s) found with a MISSING ROLE."),n.length&&r.push(`Table(s) found with an unexpected role: ${o}.`),i||n.length||r.push("No issues found.");return{findings:a,html:e.makeSection("Tables",`\n <ul>${r.map(t=>`<li>${e.escapeHtml(t)}</li>`).join("")}</ul>\n `)}}})(),(()=>{const e=window.A11Y_EMAIL,t=e=>(e.textContent||"").replace(/\s+/g," ").trim(),a=t=>Array.from(t.querySelectorAll("img")).filter(e.visible),i=t=>Array.from(t.children).flatMap(e=>e.matches?.("tr")?[e]:e.matches?.("tbody,thead,tfoot")?Array.from(e.children).filter(e=>e.matches?.("tr")):[]).filter(a=>e.visible(a)&&a.closest("table")===t),n=e=>(e||"").replace(/\s+/g," ").replace(/\u00a0/g," ").trim(),o=e=>{const t=e.cloneNode(!0);return t.querySelectorAll("table").forEach(e=>e.remove()),t.querySelectorAll("img,svg,picture,source").forEach(e=>e.remove()),n(t.textContent||"")},r=(t,a)=>Array.from(t.querySelectorAll("img")).filter(t=>e.visible(t)&&t.closest("table")===a),l=(e,t)=>{const a=r(e,t).length>0,i=Boolean(o(e)),n=((e,t)=>{const a=o(e),i=r(e,t),n=`${e.getAttribute("style")||""} ${e.style?.cssText||""}`,l=parseInt(e.getAttribute("width")||"0",10),s="function"==typeof e.getBoundingClientRect?e.getBoundingClientRect():{width:0,height:0},d=l>0&&l<=24||s.width>0&&s.width<=24,c=/font-size:\s*0|line-height:\s*0|height:\s*(?:0|10|15|20)px/i.test(n);return!a&&0===i.length||d&&!a||c})(e,t);return{hasImage:a,hasText:i,hasContent:a||i,isSpacer:n}},s=(e,t)=>{const a=(e=>Array.from(e.children).filter(e=>e.matches?.("td,th")))(e);if(!a.length)return"spacer-row";const o=a.map(e=>({cell:e,meta:l(e,t)})).filter(e=>e.meta.hasContent||!e.meta.isSpacer);if(!o.length||o.every(e=>e.meta.isSpacer||!e.meta.hasContent))return"spacer-row";const r=o.filter(e=>e.meta.hasContent&&!e.meta.isSpacer);if(r.length>=2){const e=r[0].meta,t=r[r.length-1].meta;if(e.hasImage&&!e.hasText&&!t.hasImage&&t.hasText)return"list-row"}const d=((e,t)=>{const a=Array.from(e.querySelectorAll("table")).filter(e=>e!==t&&e.closest("table")===t);if(1!==a.length)return null;const i=n(e.textContent||""),o=n(a[0].textContent||"");return i&&o&&i!==o?null:a[0]})(e,t);if(d){const e=i(d);if(1===e.length){const t=s(e[0],d);if("list-row"===t)return"list-row";if("spacer-row"===t)return"spacer-row"}}return"other-row"},d=e=>{const t=i(e);let a=0,n=0,o=!1;for(const i of t){const t=s(i,e);"list-row"!==t?"spacer-row"!==t&&(n=0):(n+=1,a=Math.max(a,n),o=!0)}return a},c=i=>{const n=i.querySelector(":scope > tbody");if(!n)return{hasListRole:!1,correct:!1,partial:!1,itemCount:0};if("list"!==(n.getAttribute("role")||"").toLowerCase())return{hasListRole:!1,correct:!1,partial:!1,itemCount:0};const o=Array.from(n.querySelectorAll(":scope > tr")).filter(t=>e.visible(t)&&t.closest("table")===i);let r=0,l=!1;for(const e of o){const i=(e.getAttribute("role")||"").toLowerCase(),n="true"===e.getAttribute("aria-hidden"),o=!t(e)&&0===a(e).length;"listitem"!==i?o&&n||(l=!0):r+=1}const s=!l&&r>=2;return r<2&&(l=!0),{hasListRole:!0,correct:s,partial:l,itemCount:r}};e.checkVisualLists=()=>{const t=Array.from(document.querySelectorAll("table")).filter(e.visible),a=[];let i=!1,n=!1;for(const e of t){const t=d(e),o=c(e),r=t>=2;if(!r&&!o.hasListRole)continue;i=i||r||o.hasListRole,e.dataset.a11yTable="true",e.dataset.a11yTableLabel=r?"Possible visual list":"list";const l=r&&!o.correct||o.partial||o.hasListRole&&o.itemCount<2;l?(n=!0,e.dataset.a11yTableStatus="issue",e.classList.add("a11y-table-issue"),e.classList.remove("a11y-table-highlight","a11y-table-possible-list"),e.setAttribute("data-text","List coding issue")):(e.dataset.a11yTableStatus=r?"info":"ok",e.classList.remove("a11y-table-issue"),e.removeAttribute("data-text"),e.classList.add(r?"a11y-table-possible-list":"a11y-table-highlight")),a.push({el:e,isVisualList:r,hasIssue:l,coding:o})}return{findings:a,html:e.makeSection("Visual lists",`\n <ul>\n <li>${n?"Issues found.":"No issues found."}</li>\n <li>${i?"List(s) found which require visual conformation.":"A visual check is in order."}</li>\n </ul>\n `)}}})(),(()=>{const e=window.A11Y_EMAIL,t=e=>{if(!e)return null;const t=Array.from(document.querySelectorAll(`input[type="radio"][name="${e.replaceAll('"','\\"')}"]`));return t.length?t.find(e=>e.checked)||t[0]:null};e.renderTabStopsOverlay=t=>{e.removeById(e.ids.svg);const a=document.createElementNS("http://www.w3.org/2000/svg","svg");a.id=e.ids.svg,a.setAttribute("aria-hidden","true"),Object.assign(a.style,{position:"absolute",inset:"0",overflow:"visible",zIndex:"10000",pointerEvents:"none",margin:"0"});const i=e=>document.createElementNS("http://www.w3.org/2000/svg",e),n=i("g"),o=i("g"),r=[];for(const e of t){const t=e.el.getBoundingClientRect(),a=Math.round(t.left+t.width/2+window.scrollX),i=Math.round(t.top+t.height/2+window.scrollY);r.push({finding:e,x:a,y:i})}for(let e=1;e<r.length;e+=1){const t=r[e-1],a=r[e],o=i("line");o.setAttribute("x1",String(t.x)),o.setAttribute("y1",String(t.y)),o.setAttribute("x2",String(a.x)),o.setAttribute("y2",String(a.y)),o.setAttribute("class","line"),n.append(o)}for(const{finding:e,x:t,y:a}of r){const n=i("rect");n.setAttribute("x",String(t-12)),n.setAttribute("y",String(a-12)),n.setAttribute("width","24"),n.setAttribute("height","24"),o.append(n);const r=i("text");r.setAttribute("x",String(t)),r.setAttribute("y",String(a)),r.setAttribute("text-anchor","middle"),r.setAttribute("dominant-baseline","central"),r.textContent=String(e.order),o.append(r),e.el.dataset.a11yTabstopHandlers||(e.el.dataset.a11yTabstopHandlers="true",e.el.addEventListener("focus",()=>n.classList.add("-js-focused")),e.el.addEventListener("blur",()=>n.classList.remove("-js-focused")))}a.append(n),a.append(o),e.ensureBody().appendChild(a)},e.checkTabStops=()=>{const a=Array.from(document.querySelectorAll(':is(a[href],area[href],audio[controls],button,embed[src],iframe,input,select,summary,textarea,video,[contenteditable],[tabindex]):not([hidden],:disabled,[tabindex^="-"])')),i=new Set,n=[];let o=0;for(const r of a){if(!e.visible(r))continue;let a=r;if(r instanceof HTMLInputElement&&"radio"===r.type){const n=r.name||"";if(!n||i.has(n))continue;const o=t(n);if(!o||!e.visible(o))continue;i.add(n),a=o}a.classList.add("-js-tabable"),o+=1,n.push({el:a,order:o})}const r=document.querySelectorAll("table").length>=4||Boolean(document.querySelector('[aria-label="Santander email"], [aria-label="Santander"]')),l=r?Array.from(document.querySelectorAll("[tabindex]")).filter(t=>e.visible(t)&&""!==String(t.getAttribute("tabindex")||"").trim()):[];for(const e of l)e.classList.add("a11y-link-issue"),e.style.outline="2px dotted #c00000",e.style.outlineOffset="2px",e.setAttribute("data-text",`tabindex=${e.getAttribute("tabindex")}`);return{findings:n,html:[e.makeSection("Keyboard tab-stops",`\n <p>Using only the keyboard, navigate through the document using <kbd>Tab</kbd> and <kbd>Shift</kbd> + <kbd>Tab</kbd>, or the arrow keys on radio buttons. Activate links by pressing <kbd>Return</kbd>, and buttons with <kbd>Return</kbd> or <kbd>Spacebar</kbd>.</p>\n <p>Reporting is state dependant, remember to <strong>expand accordions</strong> before testing.</p>\n ${r?`<p class="muted">- ${l.length?`Tabindex found in email: ${e.escapeHtml(String(l.length))}. There should be none.`:"No tabindex found in email."}</p>`:""}\n <ol>\n <li>When an element is focused by keyboard, is your current position clearly indicated?</li>\n <li>Are all interactive elements reachable and usable with just the keyboard?</li>\n <li>Does keyboard focus stop on non-interactive elements?</li>\n <li>Is any tab stop completely obscured by the overlaid yellow square marker?</li>\n <li>Does the keyboard tab order proceed in a logical sequence from left to right and top to bottom?</li>\n <li>Are there any points where the keyboard becomes stuck?</li>\n <li>Are there any elements which remain invisible upon keyboard focus?</li>\n </ol>\n `)].join("")}}})(),(()=>{const e=window.A11Y_EMAIL;e.applyTextSpacing=()=>{e.injectStyles("\n.a11y-textspacing, .a11y-textspacing *{line-height:1.5 !important;letter-spacing:0.12em !important;word-spacing:0.16em !important}\nfont-family:inherit !important;font-size:inherit !important;font-weight:inherit;font-style:inherit}\n.a11y-textspacing p,.a11y-textspacing li,.a11y-textspacing td,.a11y-textspacing th,.a11y-textspacing a,.a11y-textspacing span,.a11y-textspacing div{word-break:normal;overflow-wrap:anywhere}\n#a11y-email-console-ui.a11y-textspacing-exempt, #a11y-email-console-ui.a11y-textspacing-exempt *{line-height:normal !important;letter-spacing:normal !important;word-spacing:normal !important}\n"),e.ensureBody().classList.add("a11y-textspacing"),document.getElementById("a11y-email-console-ui")?.classList.add("a11y-textspacing-exempt");return{findings:[],html:e.makeSection("Maximum text spacing",'\n <p>The page has been temporarily styled with WCAG text-spacing values.</p>\n <ul>\n <li><code>line-height: 1.5</code></li>\n <li><code>letter-spacing: 0.12em</code></li>\n <li><code>word-spacing: 0.16em</code></li>\n </ul>\n <p class="muted">Review the page visually for clipping, overlap, and reflow issues.</p>\n ')}},e.clearTextSpacing=()=>{e.ensureBody().classList.remove("a11y-textspacing"),document.getElementById("a11y-email-console-ui")?.classList.remove("a11y-textspacing-exempt")}})(),(()=>{const e=window.A11Y_EMAIL;function t(){e.clearAll(),e.clearTextSpacing?.()}function a(a={}){t();const i={...e.defaults,...a};if(!Object.entries(i).some(([e,t])=>"baseFontSize"!==e&&!0===t))return e.toggleReport(""),e.injectStyles(e.reportShellCss),{settings:i,sectionsRendered:0,clear:t};i.tableLinearise&&e.setTableLinearise(!0);const n=[];let o=[],r=null;if(i.headings&&(r=e.checkLargeText(i.baseFontSize),n.push(e.checkHeadings({largeTextFindings:r?r.findings:[]}).html)),i.links&&n.push(e.checkLinks().html),i.tabStops){const t=e.checkTabStops();n.push(t.html),o=t.findings}return i.emailMeta&&n.push(e.checkEmailMeta().html),i.images&&n.push(e.checkImages().html),i.tables&&n.push(e.checkTables({linearise:!1}).html),i.tableLinearise&&n.push(e.makeSection("Linearised tables","<p>- All table cells are linearised into a single column. Please check that the content order still makes sense.</p>")),i.textSpacing&&n.push(e.applyTextSpacing().html),function(t){const a=['<p class="a11y-report-intro">For each check, read and answer the questions shown.</p>',...t].join("");e.toggleReport(a),e.injectStyles(e.reportShellCss)}(n),e.renderGeometryOverlay?.(),i.tabStops&&e.renderTabStopsOverlay(o),{settings:i,sectionsRendered:n.length,clear:t}}function i(){document.getElementById("a11y-email-console-ui")?.remove(),document.getElementById("a11y-email-console-ui-style")?.remove();const i=document.createElement("aside");i.id="a11y-email-console-ui",i.innerHTML=`\n <div class="a11y-console-card">\n <div class="a11y-console-head" data-drag-handle="true">\n <strong>Email accessibility testing suite</strong>\n </div>\n <p class="a11y-console-copy">Tick a check to turn it on. Untick all checks to clear the overlay.</p>\n <div class="a11y-console-grid">\n <div class="a11y-console-list">\n <div class="a11y-console-subhead">Squadies</div>\n <label><input type="checkbox" name="tableLinearise"> Linearise tables</label>\n <label><input type="checkbox" name="images"> Image alts</label>\n <label><input type="checkbox" name="links"> Links</label>\n <label><input type="checkbox" name="headings"> Headings and large text</label>\n </div>\n <div class="a11y-console-list">\n <div class="a11y-console-subhead">Developers</div>\n <label><input type="checkbox" name="emailMeta"> Email shell</label>\n <label><input type="checkbox" name="tables"> Tables</label>\n <label><input type="checkbox" name="tabStops"> Keyboard tab-stops</label>\n <label><input type="checkbox" name="textSpacing"> Text spacing</label>\n </div>\n <label class="a11y-console-font">Base font size <input type="number" min="1" step="1" name="baseFontSize" value="16"></label>\n </div>\n <div class="a11y-console-divider" aria-hidden="true"></div>\n </div>\n <output id="${e.ids.report}" class="a11y-report" role="region" aria-label="Accessibility report"></output>\n `,document.body.appendChild(i);const n=document.createElement("style");n.id="a11y-email-console-ui-style",n.textContent=`\n #a11y-email-console-ui{position:fixed;top:16px;right:16px;left:auto;z-index:2147483647;font:400 14px/1.4 Arial,sans-serif;color:#222;width:440px;max-width:min(440px,calc(100vw - 32px));height:calc(100vh - 32px);display:flex;flex-direction:column;user-select:text;background:#fff;border:2px solid #ccc;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);overflow:hidden}\n #a11y-email-console-ui .a11y-console-card{background:#fff;border:0;border-radius:0;box-shadow:none;padding:12px 12px 8px;flex:none}\n #a11y-email-console-ui .a11y-console-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;cursor:move;user-select:none}\n #a11y-email-console-ui .a11y-console-copy{margin:0 0 24px}\n #a11y-email-console-ui .a11y-console-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px 16px;align-items:start}\n #a11y-email-console-ui .a11y-console-list{display:grid;gap:8px;align-content:start}.a11y-console-subhead{font-weight:700;margin:0 0 6px}\n #a11y-email-console-ui label{display:flex;align-items:center;gap:8px;min-width:0}\n #a11y-email-console-ui .a11y-console-font{justify-content:space-between;grid-column:1 / -1;margin-top:2px}\n #a11y-email-console-ui input[type="number"]{width:72px;padding:4px}\n #a11y-email-console-ui .a11y-console-divider{margin-top:10px;border-top:1px solid #d6d6d6}\n #${e.ids.report}{display:block;background:transparent !important;box-shadow:none;border:0 !important;border-radius:0;box-sizing:border-box;padding:0 !important;margin:0;flex:1;min-height:0;overflow:auto;font:400 12px/1.45 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#222;user-select:text}\n #${e.ids.report} .a11y-report-intro{margin:40px 0 24px;padding:0;color:#333}\n #${e.ids.report} h2{text-align:left;font:600 17px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:48px 0 24px;padding:0}\n #${e.ids.report} h3{text-align:left;font:600 14px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:36px 0 20px;padding:0}\n #${e.ids.report} p,#${e.ids.report} ol,#${e.ids.report} ul,#${e.ids.report} table,#${e.ids.report} .a11y-section>div{padding:0}\n #${e.ids.report} ol,#${e.ids.report} ul{margin:8px 0 0 20px}\n #${e.ids.report} .indent{margin-left:0 !important;padding-left:0 !important;list-style:none}\n #${e.ids.report} ol.a11y-compact-list,#${e.ids.report} ul.a11y-compact-list{margin:8px 0 0 0;padding:0;list-style:none}\n #${e.ids.report} li{margin:4px 0}\n #${e.ids.report} code,#${e.ids.report} kbd{background:#fff;border:1px solid #ccc;border-radius:4px;padding:0 .3em}\n #${e.ids.report} .warn{color:#c00000;font-weight:600}\n #${e.ids.report} .ok{color:#087f23;font-weight:600}\n #${e.ids.report} .muted{color:#555}\n #${e.ids.report} table{border-collapse:collapse;width:100%;margin:0}\n #${e.ids.report} td,#${e.ids.report} th{border:1px solid #ddd;padding:4px 6px;vertical-align:top}\n #${e.ids.report} .a11y-section{margin-bottom:16px}\n #${e.ids.report} .a11y-status-pill{display:inline-flex;align-items:center;justify-content:center;min-width:2.4em;padding:2px 8px;border-radius:999px;background:#666;color:#fff;font:700 12px/1 system-ui;white-space:nowrap;flex:none}\n #${e.ids.report} .a11y-status-pill.info,#${e.ids.report} .a11y-large-size-pill{background:#0057d9 !important;color:#fff !important}\n #${e.ids.report} .a11y-large-prompt{color:#0057d9;font-weight:500}\n #${e.ids.report} .a11y-report-linkname{color:#0b5e20 !important;text-decoration:underline !important;font-weight:400 !important}\n `,document.head.appendChild(n);const o=()=>{const i=function(){const t=document.getElementById("a11y-email-console-ui");if(!t)return{...e.defaults};const a=e=>t.querySelector(`[name="${e}"]`);return{headings:!!a("headings")?.checked,links:!!a("links")?.checked,tabStops:!!a("tabStops")?.checked,emailMeta:!!a("emailMeta")?.checked,images:!!a("images")?.checked,tables:!!a("tables")?.checked,tableLinearise:!!a("tableLinearise")?.checked,textSpacing:!!a("textSpacing")?.checked,lists:!!a("lists")?.checked,baseFontSize:Number.parseFloat(a("baseFontSize")?.value||"16")||16}}();if(Object.entries(i).some(([e,t])=>"baseFontSize"!==e&&!0===t))a(i);else{t();const a=document.getElementById(e.ids.report);a&&(a.innerHTML=""),e.injectStyles(e.reportShellCss)}};i.addEventListener("change",e=>{const t=e.target;t instanceof HTMLInputElement&&("checkbox"!==t.type&&"baseFontSize"!==t.name||o())});const r=i.querySelector('[data-drag-handle="true"]');let l=!1,s=0,d=0,c=16,p=16;const m=e=>{if(!l)return;const t=e.clientX-s,a=e.clientY-d;var n;i.style.left=`${Math.max(8,Math.min(window.innerWidth-(n=i,n.getBoundingClientRect().width)-8,c+t))}px`,i.style.top=`${Math.max(8,Math.min(window.innerHeight-(e=>e.getBoundingClientRect().height)(i)-8,p+a))}px`},u=()=>{l=!1,document.removeEventListener("pointermove",m),document.removeEventListener("pointerup",u)};return r?.addEventListener("pointerdown",e=>{l=!0,s=e.clientX,d=e.clientY;const t=i.getBoundingClientRect();c=t.left,p=t.top,i.style.right="auto",document.addEventListener("pointermove",m),document.addEventListener("pointerup",u)}),i}window.A11Y_EMAIL_CONSOLE={run:a,clear:t,ui:i,defaults:{...e.defaults},root:e},i()})();
Email testing - uncompressed snippet code (for the devs)
/* A11Y Email console snippet generated from Firefox build v31. Paste into DevTools console. */
(() => {
const root = (window.A11Y_EMAIL = window.A11Y_EMAIL || {});
const ids = (root.ids = {
report: 'a11y-email-report',
styles: 'a11y-email-styles',
svg: 'a11y-email-tabstops-svg',
imageOverlay: 'a11y-email-image-overlay',
listOverlay: 'a11y-email-list-overlay',
headingsOverlay: 'a11y-email-headings-overlay',
overlay: 'a11y-email-geometry-overlay'
});
root.defaults = {
headings: false,
links: false,
tabStops: false,
emailMeta: false,
images: false,
tables: false,
tableLinearise: false,
textSpacing: false,
baseFontSize: 16
};
root.escapeHtml = (value) => String(value ?? '')
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
root.escapeAttr = root.escapeHtml;
root.ensureHead = () => {
if (document.head) return document.head;
const head = document.createElement('head');
document.documentElement.prepend(head);
return head;
};
root.ensureBody = () => {
if (document.body) {
const position = getComputedStyle(document.body).position;
if (position === 'static') document.body.style.position = 'relative';
return document.body;
}
const body = document.createElement('body');
body.style.position = 'relative';
document.documentElement.appendChild(body);
return body;
};
root.removeById = (id) => {
document.getElementById(id)?.remove();
};
root.isInternalElement = (el) => {
if (!el || !(el instanceof Element)) return false;
return Boolean(el.closest(`#${ids.report}, #a11y-email-console-ui, #${ids.overlay}, #${ids.imageOverlay}, #${ids.listOverlay}, #${ids.headingsOverlay}`));
};
root.isTrackingPixel = (el) => {
if (!(el instanceof HTMLImageElement)) return false;
const rect = el.getBoundingClientRect();
const w = Math.round(rect.width);
const h = Math.round(rect.height);
if ((w <= 1 && h <= 1) || (el.width <= 1 && el.height <= 1)) return true;
const src = (el.getAttribute('src') || '').toLowerCase();
const alt = (el.getAttribute('alt') || '').trim();
return (src.includes('tracking') || src.includes('openrate') || src.includes('pixel')) && !alt;
};
root.isMeaningfulVisibleContent = (el) => {
if (!(el instanceof Element)) return false;
if (root.isInternalElement(el) || !root.visible(el)) return false;
if (el.matches('script,style,meta,link,noscript,template')) return false;
if (el.matches('img') && root.isTrackingPixel(el)) return false;
if (el.children.length === 0) {
const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
return Boolean(text) || el.matches('img,svg,button,input,select,textarea,a,video,audio,iframe');
}
return false;
};
root.visible = (el) => {
if (!el || !(el instanceof Element)) return false;
if (root.isInternalElement(el)) return false;
const node = el;
if (typeof node.checkVisibility === 'function') {
try { return node.checkVisibility(); } catch {}
}
const style = getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
root.getDirectText = (el) => Array.from(el.childNodes)
.filter((node) => node.nodeType === Node.TEXT_NODE)
.map((node) => String(node.textContent || '').replace(/\s+/g, ' ').trim())
.filter(Boolean)
.join(' ')
.trim();
root.getFontSize = (el) => {
const size = parseFloat(getComputedStyle(el).fontSize);
return Number.isFinite(size) ? size : 0;
};
root.getAccessibleName = (el) => {
if (!(el instanceof Element)) return '';
if (root.isInternalElement(el)) return '';
const ariaLabel = el.getAttribute('aria-label');
if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim();
const labelledby = el.getAttribute('aria-labelledby');
if (labelledby) {
const parts = labelledby
.split(/\s+/)
.map((id) => document.getElementById(id)?.textContent || '')
.map((text) => text.replace(/\s+/g, ' ').trim())
.filter(Boolean);
if (parts.length) return parts.join(' ').trim();
}
const title = el.getAttribute('title');
if (title && title.trim()) return title.trim();
const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
if (text) return text;
const img = el.querySelector?.('img[alt]');
const alt = img ? (img.getAttribute('alt') || '').trim() : '';
return alt || '';
};
root.makeSection = (title, bodyHtml) => `<section class="a11y-section"><h2>${root.escapeHtml(title)}</h2>${bodyHtml}</section>`;
root.annotationText = (prefix, value) => `${prefix}: "${value}"`;
root.clearHeadingAnnotations = () => {
document.querySelectorAll('[data-a11y-heading]').forEach((el) => {
el.removeAttribute('data-a11y-heading');
el.removeAttribute('data-a11y-heading-level');
el.removeAttribute('data-a11y-heading-order');
el.removeAttribute('data-a11y-heading-label');
el.removeAttribute('data-a11y-heading-kind');
el.removeAttribute('data-text');
el.classList.remove('a11y-heading-highlight', 'a11y-heading-issue', 'a11y-heading-alt');
});
};
root.clearLargeTextAnnotations = () => {
document.querySelectorAll('[data-a11y-large-text]').forEach((el) => {
el.removeAttribute('data-a11y-large-text');
el.removeAttribute('data-a11y-large-text-label');
el.removeAttribute('data-a11y-large-text-size');
el.classList.remove('a11y-large-text-highlight', 'a11y-large-text-issue');
});
};
root.clearTableAnnotations = () => {
document.querySelectorAll('[data-a11y-table]').forEach((el) => {
el.removeAttribute('data-a11y-table');
el.removeAttribute('data-a11y-table-label');
el.removeAttribute('data-a11y-table-status');
el.classList.remove('a11y-table-highlight', 'a11y-table-issue', 'a11y-table-possible-list');
});
document.querySelectorAll('[data-a11y-table-row-issue], [data-a11y-table-body-issue]').forEach((el) => {
el.removeAttribute('data-a11y-table-row-issue');
el.removeAttribute('data-a11y-table-body-issue');
el.classList.remove('a11y-table-row-issue', 'a11y-table-body-issue');
if (el instanceof HTMLElement) {
el.style.outline = '';
el.style.outlineOffset = '';
}
});
};
root.clearImageAnnotations = () => {
document.querySelectorAll('[data-a11y-image]').forEach((el) => {
el.removeAttribute('data-a11y-image');
el.removeAttribute('data-a11y-image-label');
el.removeAttribute('data-a11y-image-status');
el.classList.remove('a11y-image-present', 'a11y-image-empty', 'a11y-image-missing');
if (el instanceof HTMLElement) {
el.style.outline = '';
el.style.outlineOffset = '';
}
});
root.removeById(ids.imageOverlay);
root.removeById(ids.overlay);
};
root.clearLinkAnnotations = () => {
document.querySelectorAll('[data-textlink], [data-a11y-link]').forEach((el) => {
el.removeAttribute('data-textlink');
el.removeAttribute('data-a11y-link');
el.removeAttribute('data-a11y-link-label');
el.classList.remove('a11y-link-highlight', 'a11y-link-issue', 'a11y-link-ok', 'a11y-link-focus', 'a11y-link-image-label', 'a11y-link-image-missing', 'a11y-link-image-wrap');
if (el instanceof HTMLElement) {
el.style.outline = '';
el.style.outlineOffset = '';
el.style.display = '';
}
el.querySelectorAll?.('[data-a11y-link-image]').forEach((img) => {
img.removeAttribute('data-a11y-link-image');
if (img instanceof HTMLElement) {
img.style.outline = '';
img.style.outlineOffset = '';
}
});
});
};
root.clearAll = () => {
root.removeById(ids.report);
root.removeById(ids.styles);
root.removeById(ids.svg);
root.removeById(ids.imageOverlay);
root.removeById(ids.overlay);
root.removeById(ids.listOverlay);
root.removeById(ids.headingsOverlay);
root.clearHeadingAnnotations();
root.clearLargeTextAnnotations();
root.clearTableAnnotations();
document.body?.classList.remove('a11y-table-linearise');
root.clearImageAnnotations();
root.clearLinkAnnotations();
document.querySelectorAll('[data-a11y-annotate], [data-text], [data-textlink], [data-a11y-list], [data-a11y-tabable], [data-a11y-tabstop-handlers], [data-a11y-link-image]').forEach((el) => {
el.removeAttribute('data-a11y-annotate');
el.removeAttribute('data-text');
el.removeAttribute('data-textlink');
el.removeAttribute('data-a11y-list');
el.removeAttribute('data-a11y-tabable');
el.removeAttribute('data-a11y-list-possible');
el.removeAttribute('data-a11y-table-row-issue');
el.removeAttribute('data-a11y-table-body-issue');
el.removeAttribute('data-a11y-tabstop-handlers');
el.classList.remove('-js-tabable', 'a11y-heading-highlight', 'a11y-heading-issue', 'a11y-heading-alt', 'a11y-large-text-highlight', 'a11y-large-text-issue', 'a11y-table-highlight', 'a11y-table-issue', 'a11y-table-possible-list', 'a11y-table-row-issue', 'a11y-table-body-issue', 'a11y-image-present', 'a11y-image-empty', 'a11y-image-missing', 'a11y-link-highlight', 'a11y-link-issue', 'a11y-link-ok', 'a11y-link-focus', 'a11y-link-image-label', 'a11y-link-image-missing', 'a11y-link-image-wrap', 'a11y-possible-list-highlight');
if (el instanceof HTMLElement) {
el.style.outline = '';
el.style.outlineOffset = '';
}
});
root.clearTextSpacing?.();
};
root.setTableLinearise = (enabled) => {
const body = root.ensureBody();
document.querySelectorAll('.a11y-preserve-hidden').forEach((el) => el.classList.remove('a11y-preserve-hidden'));
document.querySelectorAll('table[data-a11y-linearise-width]').forEach((table) => {
table.style.removeProperty('--a11y-linearise-width');
table.removeAttribute('data-a11y-linearise-width');
});
if (enabled) {
document.querySelectorAll('table').forEach((table) => {
const width = Math.round(table.getBoundingClientRect().width);
if (width > 0) {
table.setAttribute('data-a11y-linearise-width', String(width));
table.style.setProperty('--a11y-linearise-width', `${width}px`);
}
});
document.querySelectorAll('table, table *').forEach((el) => {
try {
const style = getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || el.hidden || el.getAttribute('aria-hidden') === 'true') {
el.classList.add('a11y-preserve-hidden');
}
} catch {}
});
}
body.classList.toggle('a11y-table-linearise', Boolean(enabled));
};
root.reportShellCss = `
#a11y-email-report{font:400 14px/1.5 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#222;background:transparent;border:0;margin:0;padding:0;display:block;max-width:100%;box-sizing:border-box}
#a11y-email-report h2{font:600 18px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:0 0 8px}
#a11y-email-report h3{font:600 15px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:12px 0 6px}
#a11y-email-report ol,#a11y-email-report ul{margin:8px 0 0 20px;padding:0}
#a11y-email-report li{margin:4px 0}
#a11y-email-report code,#a11y-email-report kbd{background:#fff;border:1px solid #ccc;border-radius:4px;padding:0 .3em}
#a11y-email-report .warn{color:#c00000;font-weight:600}
#a11y-email-report .ok{color:#087f23;font-weight:600}
#a11y-email-report .muted{color:#555}
#a11y-email-report table{border-collapse:collapse;width:100%}
#a11y-email-report td,#a11y-email-report th{border:1px solid #ddd;padding:4px 6px;vertical-align:top}
#a11y-email-report .a11y-section{margin-bottom:16px}
#a11y-email-report .a11y-annotation-prefix{font-weight:400}
#a11y-email-report .a11y-annotation-quote{font-style:italic}
#a11y-email-report .a11y-annotation-note{color:#b00020;font-weight:600}
#a11y-email-report .a11y-compact-list{margin:8px 0 0 0;padding:0 !important;list-style:none}
#a11y-email-report .a11y-compact-list li{margin:4px 0}
#a11y-email-report .a11y-report-linkname{color:#0b5e20 !important;text-decoration:underline !important;font-weight:400 !important}
#a11y-email-report .a11y-status-pill{display:inline-flex;align-items:center;min-width:2.2em;padding:2px 8px;border-radius:999px;background:#555;color:#fff;font:700 11px/1 system-ui;white-space:nowrap}
#a11y-email-report .a11y-status-pill.ok{background:#087f23}
#a11y-email-report .a11y-status-pill.warn{background:#b00020}
#a11y-email-report .a11y-status-pill.info{background:#0057d9;color:#fff}
#a11y-email-report .a11y-large-size-pill{background:#0057d9 !important;color:#fff !important}
#a11y-email-report .a11y-large-prompt{color:#0057d9;font-weight:500}
#a11y-email-report .a11y-report-linkname{color:#0b5e20 !important;text-decoration:underline !important;font-weight:400 !important}
.a11y-inline-badge{position:absolute;inset:0 auto auto 0;z-index:10;pointer-events:none;display:inline-flex;align-items:flex-start;justify-content:flex-start;gap:4px;background:#0057d9;color:#fff;padding:2px 6px;font:600 12px/1.2 system-ui;border-radius:3px;box-shadow:0 0 0 1px rgba(0,0,0,.15);max-width:calc(100vw - 24px);white-space:nowrap}
.a11y-highlight{outline:3px dotted #0057d9 !important;outline-offset:2px !important;position:relative}
.a11y-highlight-ok{outline:3px solid #087f23 !important;outline-offset:2px !important;position:relative}
.a11y-focus{outline:3px dashed #0057d9 !important;outline-offset:2px !important}
#a11y-email-tabstops-svg{position:absolute;inset:0;overflow:visible;z-index:10000;pointer-events:none;margin:0}
#a11y-email-tabstops-svg .line{stroke:#0057d9;stroke-width:4px}
#a11y-email-tabstops-svg rect{fill:#0057d9;stroke:#0057d9;stroke-width:1px}
#a11y-email-tabstops-svg text{fill:#fff;font-size:10px;font-weight:700}
#a11y-email-tabstops-svg rect.-js-focused{fill:#0057d9 !important}
.a11y-textspacing, .a11y-textspacing *{line-height:1.5 !important;letter-spacing:0.12em !important;word-spacing:0.16em !important}
font-family:inherit !important;font-size:inherit !important;font-weight:inherit;font-style:inherit}
.a11y-textspacing p,.a11y-textspacing li,.a11y-textspacing td,.a11y-textspacing th,.a11y-textspacing a,.a11y-textspacing span,.a11y-textspacing div{word-break:normal;overflow-wrap:anywhere}
.a11y-table-linearise table{display:block !important;width:var(--a11y-linearise-width, auto) !important;max-width:100% !important;box-sizing:border-box}.a11y-table-linearise tbody,.a11y-table-linearise tr,.a11y-table-linearise table,.a11y-table-linearise td,.a11y-table-linearise th{display:block !important;width:100% !important;box-sizing:border-box;text-align:left !important;clear:both !important}
.a11y-table-linearise .a11y-preserve-hidden{display:none !important}
.a11y-table-linearise [hidden],.a11y-table-linearise [aria-hidden="true"]{display:none !important}
.a11y-table-linearise td,.a11y-table-linearise th{border-left:0 !important;border-right:0 !important}
.a11y-table-linearise table{margin-bottom:12px}
.a11y-table-linearise tr{padding:4px 0}
.a11y-table-linearise td,.a11y-table-linearise th{padding:4px 6px}
.a11y-heading-highlight{position:relative;outline-offset:2px !important;outline:3px solid #087f23 !important}
.a11y-heading-highlight::after{content:attr(data-a11y-heading-label);position:absolute;top:0;right:0;transform:translate(4px,-50%);background:#087f23;color:#fff;padding:2px 6px;border-radius:999px;font:700 11px/1 system-ui;box-shadow:0 0 0 1px rgba(0,0,0,.15);pointer-events:none;z-index:2;white-space:nowrap}
.a11y-heading-issue{outline-style:dashed !important;outline-color:#c00000 !important}
.a11y-heading-issue::after{background:#c00000}
.a11y-heading-alt{outline-style:solid !important;outline-color:#087f23 !important}
.a11y-heading-flag{display:inline-flex;align-items:center;justify-content:center;min-width:2.4em;padding:2px 6px;border-radius:999px;background:#087f23;color:#fff;font:700 11px/1 system-ui;white-space:nowrap;flex:none}
.a11y-heading-row{display:flex;align-items:flex-start;gap:8px;margin:0 0 6px}
.a11y-heading-copy{min-width:0;flex:1}
.a11y-heading-issue-text{color:#b00020;font-weight:600}
.a11y-large-text-highlight{position:relative;outline:3px dashed #0057d9 !important;outline-offset:2px !important}
.a11y-large-text-highlight::before{content:attr(data-a11y-large-text-label);position:absolute;top:0;right:0;transform:translate(4px,-50%);background:#0057d9;color:#fff;padding:2px 6px;border-radius:999px;font:700 11px/1 system-ui;box-shadow:0 0 0 1px rgba(0,0,0,.15);pointer-events:none;z-index:2;white-space:nowrap}
.a11y-large-text-issue{outline-style:dashed !important;outline-color:#0057d9 !important}
.a11y-table-highlight{position:relative;outline:1px solid #087f23 !important;outline-offset:2px !important}
.a11y-table-highlight::before{content:attr(data-a11y-table-label);position:absolute;top:0;right:0;transform:translate(4px,-50%);background:#087f23;color:#fff;padding:2px 6px;border-radius:999px;font:700 11px/1 system-ui;box-shadow:0 0 0 1px rgba(0,0,0,.15);pointer-events:none;z-index:2;white-space:nowrap}
.a11y-table-issue{outline-style:dotted !important;outline-color:#c00000 !important}
.a11y-table-possible-list{outline:3px dashed #0057d9 !important;outline-offset:2px !important}
.a11y-table-row-issue, .a11y-table-body-issue{outline:2px dotted #c00000 !important;outline-offset:2px !important}
.a11y-image-present{outline:3px solid #087f23 !important;outline-offset:2px !important}
.a11y-image-empty{outline:3px dashed #0057d9 !important;outline-offset:2px !important}
.a11y-image-missing{outline:3px dotted #c00000 !important;outline-offset:2px !important}
.a11y-link-ok{outline:4px solid #087f23 !important;outline-offset:2px !important}
.a11y-link-focus{outline:4px dashed #0057d9 !important;outline-offset:2px !important}
.a11y-link-issue{outline:4px dotted #c00000 !important;outline-offset:2px !important;display:inline-block;position:relative}
.a11y-link-image-wrap{display:inline-block;position:relative}
.a11y-link-ok img,.a11y-link-highlight img{outline:2px solid #087f23 !important;outline-offset:2px !important}
.a11y-link-issue img{outline:2px dotted #c00000 !important;outline-offset:2px !important}
.a11y-link-focus img{outline:2px dashed #0057d9 !important;outline-offset:2px !important}
.a11y-link-image-label{position:relative;display:inline-block}
.a11y-link-image-label::before{box-sizing:border-box;content:attr(data-a11y-link-label);color:#fff;background-color:#0057d9;text-shadow:0 0 1px #000;z-index:2;align-items:flex-start;justify-content:flex-start;width:max-content;min-height:100%;padding:2px 6px;font:500 14px/1.3 sans-serif;display:flex;position:absolute;inset:0 0 auto auto;transform:translate(4px,-50%)}
[data-textlink]{--a-outline-color:#c00000;--a-outline-width:4px;--a-outline-style:dotted;--a-outline-offset:2px;display:inline-block;position:relative}
[data-textlink]:after{box-sizing:border-box;content:attr(data-textlink);color:#fff;background-color:#c00000;text-shadow:0 0 1px #000;z-index:1;align-items:center;width:max-content;min-height:100%;margin-left:8px;padding:2px 6px;font:500 14px/1.3 sans-serif;display:flex;position:absolute;inset:0 -100% auto 100%}
.a11y-possible-list-highlight{outline:3px dashed #0057d9 !important;outline-offset:2px !important}
.a11y-possible-list-highlight::before{content:'Possible visual list';position:absolute;top:0;right:0;transform:translate(4px,-50%);background:#0057d9;color:#fff;padding:2px 6px;border-radius:999px;font:700 11px/1 system-ui;box-shadow:0 0 0 1px rgba(0,0,0,.15);pointer-events:none;z-index:2;white-space:nowrap}
.a11y-tabindex-issue{outline:2px dotted #c00000 !important;outline-offset:2px !important;position:relative}
[data-text]:not(.a11y-heading-highlight):not(.a11y-image-empty){display:inline-block;position:relative;outline:2px dotted #c00000 !important;outline-offset:2px !important}
[data-text]:not(.a11y-heading-highlight):not(.a11y-image-empty)::after{box-sizing:border-box;content:attr(data-text);color:#fff;background-color:#c00000;text-shadow:0 0 1px #000;z-index:1;align-items:center;width:max-content;min-height:100%;margin-left:8px;padding:2px 6px;font:500 14px/1.3 sans-serif;display:flex;position:absolute;inset:0 -100% auto 100%}
.a11y-image-empty[data-text]{outline:2px dashed #0057d9 !important;outline-offset:2px !important;position:relative}
.a11y-image-empty[data-text]::after{background-color:#0057d9 !important}
`;
})();
(() => {
const root = window.A11Y_EMAIL;
root.injectStyles = (css) => {
const id = root.ids.styles;
let style = document.getElementById(id);
if (style) return style;
style = document.createElement('style');
style.id = id;
style.textContent = css;
root.ensureHead().appendChild(style);
return style;
};
root.toggleReport = (html) => {
let out = document.getElementById(root.ids.report);
if (!out) {
out = document.createElement('output');
out.id = root.ids.report;
out.className = 'a11y-report';
out.setAttribute('role', 'region');
out.setAttribute('aria-label', 'Accessibility report');
const panel = document.getElementById('a11y-email-console-ui');
if (panel) {
panel.appendChild(out);
} else {
root.ensureBody().prepend(out);
}
}
out.innerHTML = html;
return out;
};
root.ensureGeometryOverlay = () => {
let overlay = document.getElementById(root.ids.overlay);
if (overlay) return overlay;
overlay = document.createElement('div');
overlay.id = root.ids.overlay;
overlay.setAttribute('aria-hidden', 'true');
Object.assign(overlay.style, {
position: 'fixed',
inset: '0',
pointerEvents: 'none',
zIndex: '2147483646',
overflow: 'visible'
});
document.documentElement.appendChild(overlay);
return overlay;
};
root.makeGeometryBadge = (text, theme = 'blue') => {
const badge = document.createElement('span');
badge.className = `a11y-geometry-badge a11y-theme-${theme}`;
badge.textContent = text;
return badge;
};
root.positionGeometryBadge = (badge, target, corner = 'top-right', dx = 4, dy = 0) => {
const rect = target.getBoundingClientRect();
let left = rect.right + dx;
let top = rect.top + dy;
if (corner === 'top-left') {
left = rect.left + dx;
top = rect.top + dy;
} else if (corner === 'top-right-inside') {
left = Math.max(0, rect.right - dx);
top = rect.top + dy;
badge.style.transform = 'translate(-100%, 0)';
} else if (corner === 'mid-right') {
left = rect.right + dx;
top = rect.top + (rect.height / 2) + dy;
badge.style.transform = 'translate(0, -50%)';
}
badge.style.left = `${Math.round(left)}px`;
badge.style.top = `${Math.round(top)}px`;
};
root.renderGeometryOverlay = () => {
root.removeById(root.ids.overlay);
const overlay = root.ensureGeometryOverlay();
const handled = new WeakMap();
const add = (el, text, theme, corner = 'top-right', dx = 4, dy = 0) => {
if (!(el instanceof Element) || !root.visible(el) || !text) return;
const existing = handled.get(el) || new Set();
const key = `${theme}|${corner}|${text}`;
if (existing.has(key)) return;
existing.add(key);
handled.set(el, existing);
const badge = root.makeGeometryBadge(text, theme);
root.positionGeometryBadge(badge, el, corner, dx, dy);
overlay.appendChild(badge);
};
Array.from(document.querySelectorAll('[data-a11y-heading="true"]')).forEach((el) => {
const issue = el.classList.contains('a11y-heading-issue');
add(el, el.dataset.a11yHeadingLabel || 'H', issue ? 'red' : 'green', 'top-right-inside', 2, 0);
if (el.dataset.text) {
add(el, el.dataset.text, 'red', 'mid-right', 8, 0);
}
});
Array.from(document.querySelectorAll('[data-a11y-large-text="true"]')).forEach((el) => {
add(el, el.dataset.a11yLargeTextLabel || '', 'blue', 'top-right-inside', 2, 0);
});
Array.from(document.querySelectorAll('[data-a11y-link="true"]')).forEach((el) => {
const label = el.dataset.a11yLinkLabel || '';
const issue = el.getAttribute('data-textlink') || '';
if (label) add(el, label, el.classList.contains('a11y-link-image-missing') ? 'red' : 'green', 'top-right', 4, 0);
if (issue) add(el, issue, 'red', 'mid-right', 8, 0);
});
Array.from(document.querySelectorAll('[data-a11y-image="true"]')).forEach((el) => {
const status = el.dataset.a11yImageStatus || 'present';
const label = el.dataset.a11yImageLabel || '';
const theme = status === 'missing' ? 'red' : (status === 'empty' ? 'blue' : 'green');
if (label) add(el, label, theme, 'top-left', 0, 0);
});
Array.from(document.querySelectorAll('[data-a11y-table="true"]')).forEach((el) => {
const label = el.dataset.a11yTableLabel || '';
const theme = el.dataset.a11yTableStatus === 'issue' ? 'red' : (el.classList.contains('a11y-table-possible-list') ? 'blue' : 'green');
if (label) add(el, label, theme, 'top-right-inside', 2, 0);
if (el.dataset.text) add(el, el.dataset.text, 'red', 'mid-right', 8, 0);
});
Array.from(document.querySelectorAll('[data-text]:not([data-a11y-image]):not([data-a11y-table]):not([data-a11y-heading])')).forEach((el) => {
add(el, el.getAttribute('data-text') || '', el.classList.contains('a11y-image-empty') ? 'blue' : 'red', 'mid-right', 8, 0);
});
};
if (!window.__a11yGeometryOverlayHandlers) {
const rerender = () => {
if (document.getElementById(root.ids.overlay)) {
root.renderGeometryOverlay();
}
};
window.addEventListener('scroll', rerender, true);
window.addEventListener('resize', rerender, true);
window.__a11yGeometryOverlayHandlers = true;
}
root.reportShellCss = `
#a11y-email-report{font:400 14px/1.5 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#222;background:transparent;border:0;margin:0;padding:0;display:block;max-width:100%;box-sizing:border-box}
#a11y-email-report h2{font:600 18px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:0 0 8px}
#a11y-email-report h3{font:600 15px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:12px 0 6px}
#a11y-email-report ol,#a11y-email-report ul{margin:8px 0 0 20px;padding:0}
#a11y-email-report li{margin:4px 0}
#a11y-email-report code,#a11y-email-report kbd{background:#fff;border:1px solid #ccc;border-radius:4px;padding:0 .3em}
#a11y-email-report .warn{color:#c00000;font-weight:600}
#a11y-email-report .ok{color:#087f23;font-weight:600}
#a11y-email-report .muted{color:#555}
#a11y-email-report .a11y-status-pill{display:inline-flex;align-items:center;justify-content:center;min-width:2.4em;padding:2px 8px;border-radius:999px;background:#666;color:#fff;font:700 12px/1 system-ui;white-space:nowrap;flex:none}
#a11y-email-report .a11y-status-pill.info,#a11y-email-report .a11y-large-size-pill{background:#0057d9 !important;color:#fff !important}
#a11y-email-report .a11y-large-prompt{color:#0057d9;font-weight:500}
#a11y-email-report .a11y-report-linkname{color:#0b5e20 !important;text-decoration:underline !important;font-weight:400 !important}
.a11y-section{margin:0 0 16px}
.a11y-heading-row{display:flex;align-items:flex-start;gap:8px}
.a11y-heading-flag{display:inline-flex;align-items:center;justify-content:center;min-width:32px;padding:2px 8px;border-radius:999px;background:#087f23;color:#fff;font:700 12px/1 system-ui}
.a11y-heading-copy{display:inline-block}
.a11y-annotation-prefix{font-weight:400}
.a11y-annotation-quote{font-weight:700}
.a11y-annotation-note{color:#c00000;font-weight:700}
.a11y-compact-list{margin-left:20px}
.a11y-geometry-badge{position:fixed;pointer-events:none;display:inline-flex;align-items:flex-start;justify-content:flex-start;padding:2px 6px;font:600 12px/1.2 system-ui;border-radius:3px;box-shadow:0 0 0 1px rgba(0,0,0,.15);max-width:min(360px, calc(100vw - 16px));white-space:normal;word-break:break-word}
.a11y-theme-blue{background:#0057d9;color:#fff}
.a11y-theme-red{background:#c00000;color:#fff}
.a11y-theme-green{background:#087f23;color:#fff}
.a11y-heading-highlight{outline:2px solid #087f23 !important;outline-offset:2px !important}
.a11y-heading-issue{outline:2px dotted #c00000 !important}
.a11y-large-text-highlight{outline:2px dashed #0057d9 !important;outline-offset:2px !important}
.a11y-table-highlight{outline:1px solid #b7efc5 !important;outline-offset:2px !important}
.a11y-table-issue{outline:2px dotted #c00000 !important}
.a11y-table-possible-list{outline:2px dashed #0057d9 !important}
.a11y-link-highlight{outline:2px solid #087f23 !important;outline-offset:2px !important}
.a11y-link-issue{outline:2px dotted #c00000 !important}
.a11y-link-focus{outline:2px dashed #0057d9 !important}
.a11y-image-present{outline:2px solid #087f23 !important;outline-offset:2px !important}
.a11y-image-empty{outline:2px dashed #0057d9 !important;outline-offset:2px !important}
.a11y-image-missing{outline:2px dotted #c00000 !important;outline-offset:2px !important}
#a11y-email-tabstops-svg{--bg-color:yellow;--focus-color:#0057d9}
#a11y-email-tabstops-svg .line{stroke:var(--bg-color);stroke-width:4px}
#a11y-email-tabstops-svg rect{fill:var(--bg-color);stroke:#000;stroke-width:1px}
#a11y-email-tabstops-svg text{fill:#000;font-size:10px;font-weight:700}
#a11y-email-tabstops-svg rect.-js-focused{fill:var(--focus-color) !important}
body.a11y-table-linearise table{width:var(--a11y-linearise-width, auto) !important;max-width:100% !important;box-sizing:border-box}body.a11y-table-linearise table:not([role=main]){width:var(--a11y-linearise-width, auto) !important;max-width:var(--a11y-linearise-width, auto) !important;margin-left:auto !important;margin-right:auto !important;box-sizing:border-box}body.a11y-table-linearise table:not([role=main]),body.a11y-table-linearise table:not([role=main]) tbody,body.a11y-table-linearise table:not([role=main]) thead,body.a11y-table-linearise table:not([role=main]) tfoot,body.a11y-table-linearise table:not([role=main]) tr{display:block !important;width:100% !important;box-sizing:border-box}body.a11y-table-linearise table:not([role=main]) :is(table,th,td){display:block !important;text-align:left !important;outline:1px solid #0f0 !important;clear:both !important;margin-left:0 !important;padding-left:0 !important;width:100% !important;box-sizing:border-box}body.a11y-table-linearise .a11y-preserve-hidden{display:none !important}
body.a11y-table-linearise table:not([role=main]) tr{margin:0 0 8px !important}
body.a11y-table-linearise [hidden],body.a11y-table-linearise [aria-hidden="true"],body.a11y-table-linearise [style*="display:none"],body.a11y-table-linearise [style*="display: none"]{display:none !important}
`;
})();
(() => {
const root = window.A11Y_EMAIL;
const decodeEntities = (value) => {
const area = document.createElement('textarea');
area.innerHTML = String(value || '');
return area.value;
};
const getWholeDocumentMarkup = () => {
try {
return document.documentElement ? document.documentElement.outerHTML || '' : '';
} catch (error) {
return '';
}
};
const extractRoundrectOpenTags = (html) => {
const source = decodeEntities(String(html || ''));
return source.match(/<v:roundrect\b[^>]*>/gi) || [];
};
const getAltStateFromTag = (tagText) => {
const match = String(tagText || '').match(/\balt\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i);
if (!match) return 'missing';
const value = decodeEntities(match[1] != null ? match[1] : (match[2] != null ? match[2] : match[3] || ''));
return value === '' ? 'empty' : 'present';
};
const scanMarkupForOutlook = () => {
const html = getWholeDocumentMarkup();
const tags = extractRoundrectOpenTags(html);
if (!tags.length) {
return { foundRoundrect: false, missingAlt: false, tagsChecked: 0 };
}
let missingAlt = false;
for (const tag of tags) {
const altState = getAltStateFromTag(tag);
if (altState !== 'empty') {
missingAlt = true;
}
}
return { foundRoundrect: true, missingAlt, tagsChecked: tags.length };
};
const isAcceptableLabel = (value) => {
const label = String(value || '').trim().toLowerCase();
return label === 'santander' || label === 'santander email';
};
const visibleContentNodes = () => Array.from(document.body ? document.body.querySelectorAll('*') : [])
.filter((el) => root.visible(el))
.filter((el) => !root.isInternalElement(el))
.filter((el) => !el.closest('#a11y-email-tabstops-svg'))
.filter((el) => !el.closest('#a11y-email-console-ui'));
const outerVisibleTable = () => {
const tables = Array.from(document.querySelectorAll('table')).filter(root.visible);
if (!tables.length) return null;
return tables.find((table) => !tables.some((other) => other !== table && other.contains(table))) || tables[0];
};
root.checkEmailMeta = () => {
const findings = [];
const candidates = Array.from(document.querySelectorAll('main, [role="main"]')).filter(root.visible);
const validMain = candidates.find((el) =>
(el.getAttribute('role') || '').toLowerCase() === 'main' &&
(el.getAttribute('dir') || '').toLowerCase() === 'ltr' &&
isAcceptableLabel(el.getAttribute('aria-label'))
);
const fallbackTable = outerVisibleTable();
const container = validMain || fallbackTable || candidates[0] || document.body;
const visibleNodes = Array.from(document.body.querySelectorAll('*')).filter(root.isMeaningfulVisibleContent).filter((el) => {
if (el.closest(`#${root.ids.panel}`)) return false;
if (el instanceof HTMLImageElement) {
const w = Number(el.getAttribute('width') || 0);
const h = Number(el.getAttribute('height') || 0);
if ((w <= 1 && h <= 1) || /tracking|open/i.test(el.getAttribute('alt') || '')) return false;
}
return true;
});
const outsideNodes = visibleNodes.filter((el) => el !== container && !container.contains(el));
const checks = [
['role', (container.getAttribute('role') || '').toLowerCase() === 'main', 'Expected role="main" on a single enclosing content container'],
['dir', (container.getAttribute('dir') || '').toLowerCase() === 'ltr', 'Expected dir="ltr" on the same content container'],
['aria-label', isAcceptableLabel(container.getAttribute('aria-label')), 'Expected aria-label="Santander" or aria-label="Santander email" on the same content container'],
['encloses', outsideNodes.length === 0, outsideNodes.length === 0 ? 'OK' : 'Expected visible email content to sit within that same single content container'],
['lang', (container.getAttribute('lang') || '').toLowerCase() === 'en-gb', 'Suggestion: add lang="en-gb" to the same content container']
];
for (const [key, pass, message] of checks) {
if (!pass && key !== 'lang') findings.push({ key, issue: message, el: container });
}
const outlookScan = scanMarkupForOutlook();
const outlookIssue = outlookScan.foundRoundrect && outlookScan.missingAlt;
if (outlookIssue) {
findings.push({
key: 'outlook-roundrect',
issue: 'Email contains a hidden Outlook v:roundrect image component which is missing an alt text. Please report to the developers to repair.',
el: container
});
}
const html = root.makeSection('Email container rules', `
<p>Checks for a single enclosing HTML email container.</p>
<ul>
${checks.map(([, pass, msg]) => `<li>${pass ? '<span class="ok">OK</span>' : '<span class="warn">Issue</span>'} - ${root.escapeHtml(msg)}</li>`).join('')}
<li>${outlookIssue ? '<span class="warn">Issue</span> - Email contains a hidden Outlook v:roundrect image component which is missing an alt text. Please report to the developers to repair.' : '<span class="ok">OK</span> - No hidden Outlook v:roundrect image component issue found.'}</li>
</ul>
<p class="muted">- Container inspected: <code>${root.escapeHtml(container.tagName.toLowerCase())}</code></p>
`);
return { findings, html };
};
})();
(() => {
const root = window.A11Y_EMAIL;
const headingSelector = ':is(h1,h2,h3,h4,h5,h6,[role="heading"])';
const candidateSelector = 'body *';
const directText = (el) => root.getDirectText(el);
const qualifies = (el, baseFontSize) => {
if (!(el instanceof HTMLElement)) return false;
if (!root.visible(el)) return false;
if (el.closest('a[href]')) return false;
if (el.closest(headingSelector)) return false;
if (el.closest(`#${root.ids.report}, #${root.ids.svg}, #${root.ids.imageOverlay}`)) return false;
const text = directText(el);
if (!text) return false;
return root.getFontSize(el) > baseFontSize + 0.1;
};
root.checkLargeText = (baseFontSize = 16) => {
root.clearLargeTextAnnotations();
const candidates = Array.from(document.querySelectorAll(candidateSelector)).filter((el) => qualifies(el, baseFontSize));
const findings = [];
for (const el of candidates) {
const fontSize = root.getFontSize(el);
const parent = el.parentElement;
if (parent && parent instanceof HTMLElement && qualifies(parent, baseFontSize) && root.getFontSize(parent) >= fontSize) {
continue;
}
const text = directText(el);
if (!text) continue;
el.dataset.a11yLargeText = 'true';
el.dataset.a11yLargeTextLabel = `${Math.round(fontSize)}px`;
el.dataset.a11yLargeTextSize = String(Math.round(fontSize));
el.classList.add('a11y-large-text-highlight');
findings.push({
el,
text,
fontSize: Math.round(fontSize),
issue: `Text is larger than the base font size (${baseFontSize}px) and is not marked as a heading`
});
}
const html = root.makeSection('Large text', `
<p>Please review the large text outlined in dashed blue and consider whether it should be marked up as a heading.</p>
<p>A heading should introduce the content that follows, so it’s perfectly fine for some text to remain styled as large text rather than being treated as a heading.</p>
<p>${findings.length ? 'Large text detected.' : 'No large text detected.'}</p>
`);
return { findings, html };
};
})();
(() => {
const root = window.A11Y_EMAIL;
const getLevel = (el) => {
const aria = el.getAttribute('aria-level');
if (aria) {
const parsed = parseInt(aria, 10);
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 6) return parsed;
}
const match = el.tagName.match(/^H([1-6])$/i);
return match ? parseInt(match[1], 10) : 6;
};
const getHeadingText = (el) => {
const text = (el.textContent || '').trim();
const img = el.querySelector('img');
const altAttr = img ? img.getAttribute('alt') : null;
const alt = altAttr === null ? null : altAttr.trim();
return { text, img, alt, altAttr };
};
const docOrder = (a, b) => {
if (!a || !b || a === b) return 0;
const pos = a.compareDocumentPosition(b);
if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
return 0;
};
root.checkHeadings = (options = {}) => {
const selector = ':is(h1,h2,h3,h4,h5,h6,[role="heading"])';
const headings = Array.from(document.querySelectorAll(selector)).filter(root.visible);
const findings = [];
const largeTextFindings = Array.isArray(options.largeTextFindings) ? options.largeTextFindings : [];
const visibleH1s = headings.filter((el) => getLevel(el) === 1);
let previous = 0;
let h1Count = 0;
let order = 0;
root.clearHeadingAnnotations();
for (const el of headings) {
const level = getLevel(el);
const { text, img, alt, altAttr } = getHeadingText(el);
const fromAlt = Boolean(img && !text && alt);
const missingAlt = Boolean(img && !text && altAttr === null);
const emptyAlt = Boolean(img && !text && altAttr !== null && alt === '');
let issue = '';
const labelText = `H${level}`;
let displayText = text;
order += 1;
el.dataset.a11yHeading = 'true';
el.dataset.a11yHeadingLevel = String(level);
el.dataset.a11yHeadingOrder = String(order);
el.dataset.a11yHeadingLabel = labelText;
el.classList.add('a11y-heading-highlight');
if (fromAlt) {
displayText = alt;
el.dataset.a11yHeadingAlt = 'true';
el.dataset.a11yHeadingKind = 'alt';
el.classList.add('a11y-heading-alt');
} else if (missingAlt) {
issue = 'MISSING alt text';
} else if (emptyAlt) {
issue = 'Empty alt text';
}
if (!text && !fromAlt && !img) issue = issue || `Empty H${level} heading`;
if (level === 1) {
h1Count += 1;
if (visibleH1s.length > 1 && h1Count > 1) {
issue = issue ? `${issue} Only one H1 heading is allowed.` : 'Only one H1 heading is allowed.';
}
}
if (previous && level > previous + 1) issue = issue ? `${issue} Should be a H${previous + 1}, or lower, but it's a H${level}` : `Should be a H${previous + 1}, or lower, but it's a H${level}`;
if (issue) {
el.setAttribute('data-text', issue);
el.classList.add('a11y-heading-issue');
}
findings.push({ level, text: displayText || (alt ?? '') || '(empty)', issue, el, order, fromAlt, missingAlt, emptyAlt, type: 'heading' });
previous = level;
}
if (!document.querySelector('h1, [role="heading"][aria-level="1"]')) {
findings.unshift({ level: 1, text: 'MISSING H1', issue: 'A H1 heading is a requirement', synthetic: true, order: 0, fromAlt: false, type: 'heading' });
}
const merged = [];
for (const f of findings) merged.push(f);
for (const f of largeTextFindings) merged.push({ ...f, type: 'largeText' });
merged.sort((a, b) => {
if (a.synthetic && !b.synthetic) return -1;
if (!a.synthetic && b.synthetic) return 1;
if (!a.el || !b.el) return 0;
return docOrder(a.el, b.el);
});
let currentLevel = 1;
const rows = merged.map((f) => {
if (f.type === 'largeText') {
const indent = Math.max(0, currentLevel * 18);
return `<li class="a11y-heading-row a11y-large-row" style="margin-left:${indent}px"><span class="a11y-status-pill info a11y-large-size-pill">${root.escapeHtml(String(f.fontSize))}px</span><span class="a11y-heading-copy a11y-large-copy">${root.escapeHtml(f.text)} <span class="a11y-large-prompt">- Is this a heading?</span></span></li>`;
}
currentLevel = Number(f.level) || currentLevel;
const indent = Math.max(0, (currentLevel - 1) * 18);
const chip = `<span class="a11y-heading-flag">${root.escapeHtml(`H${currentLevel}`)}</span>`;
if (f.fromAlt) {
const issueMarkup = f.issue ? ` <strong class="a11y-annotation-note">- ${root.escapeHtml(f.issue)}</strong>` : '';
return `<li class="a11y-heading-row" style="margin-left:${indent}px">${chip}<span class="a11y-heading-copy"><span class="a11y-annotation-prefix">alt=</span> <strong class="a11y-annotation-quote">"${root.escapeHtml(f.text)}"</strong>${issueMarkup}</span></li>`;
}
if (f.emptyAlt) {
return `<li class="a11y-heading-row" style="margin-left:${indent}px">${chip}<span class="a11y-heading-copy"><strong class="a11y-annotation-note">- Empty alt text</strong></span></li>`;
}
if (f.missingAlt) {
return `<li class="a11y-heading-row" style="margin-left:${indent}px">${chip}<span class="a11y-heading-copy"><strong class="a11y-annotation-note">- MISSING alt text</strong></span></li>`;
}
const issueMarkup = f.issue ? ` <strong class="a11y-annotation-note">- ${root.escapeHtml(f.issue)}</strong>` : '';
return `<li class="a11y-heading-row" style="margin-left:${indent}px">${chip}<span class="a11y-heading-copy"><strong>${root.escapeHtml(f.text)}</strong>${issueMarkup}</span></li>`;
}).join('');
const html = root.makeSection('Headings and large text', `
<p><strong>Check:</strong> heading structure, empty headings, H1 usage, heading order, headings made from image alt text, and non-heading large text.</p>
<ol>
<li>Is there a single main <code>H1</code> heading at the top of the page?</li>
<li>Are headings arranged in a logical order from H1 to H6?</li>
<li>Do all headings clearly describe the content that follows?</li>
<li>Have all empty headings been removed?</li>
<li>Please review the large text outlined in dashed blue and consider whether it should be marked up as a heading.</li>
</ol>
<p>Please review the large text outlined in dashed blue and consider whether it should be marked up as a heading.</p>
<p>A heading should introduce the content that follows, so it’s perfectly fine for some text to remain styled as large text rather than being treated as a heading.</p>
<ol class="a11y-compact-list">${rows}</ol>
`);
return { findings, html };
};
})();
(() => {
const root = window.A11Y_EMAIL;
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
const getVisibleText = (node) => {
if (!node) return '';
if (node.nodeType === Node.TEXT_NODE) return normalize(node.textContent || '');
if (node.nodeType !== Node.ELEMENT_NODE) return '';
const el = node;
if (!root.visible(el)) return '';
if (el.matches('img,svg,script,style,noscript')) return '';
const parts = [];
for (const child of Array.from(el.childNodes)) {
const text = getVisibleText(child);
if (text) parts.push(text);
}
return normalize(parts.join(' '));
};
const getLinkInfo = (link) => {
const img = link.querySelector('img');
const visibleText = getVisibleText(link);
const altAttr = img ? img.getAttribute('alt') : null;
const alt = altAttr === null ? null : normalize(altAttr);
const hasImage = Boolean(img);
const hasImageAlt = alt !== null && alt !== '';
const imageAltMissing = hasImage && !hasImageAlt;
const ariaLabel = normalize(link.getAttribute('aria-label') || '');
const accessibleName = ariaLabel || visibleText || (hasImageAlt ? alt : '');
return {
img,
visibleText,
alt,
hasImage,
hasImageAlt,
imageAltMissing,
accessibleName
};
};
const sameTextDifferentHref = (allLinks, current, text, href) => {
if (!text || !href) return false;
for (const other of allLinks) {
if (other === current) continue;
const otherHref = normalize(other.getAttribute('href') || '');
if (!otherHref) continue;
const otherText = normalize(other.dataset.a11yLinkName || '');
if (otherText && otherText === text && otherHref !== href) return true;
}
return false;
};
const isGeneric = (text) => {
const value = normalize(text).toLowerCase();
return ['see all', 'see more', 'view all', 'view more', 'read more', 'more', 'click here', 'here', 'find out more'].includes(value);
};
const isDocumentLink = (link) => {
const href = link.href || '';
if (href.startsWith('javascript:')) return false;
return ['.pdf', '.xlsx', '.docx', '.pptx'].some((ext) => href.toLowerCase().includes(ext));
};
const fileExt = (href) => {
try {
const url = new URL(href);
url.search = '';
url.hash = '';
const path = url.pathname.toLowerCase();
const match = path.match(/\.([a-z0-9]+)$/i);
return (match?.[1] || '').toLowerCase();
} catch {
return '';
}
};
root.checkLinks = () => {
root.clearLinkAnnotations();
const links = Array.from(document.querySelectorAll('a'));
const findings = [];
const issueFlags = {
missingHref: false,
duplicate: false,
imageAltMissing: false
};
for (const link of links) {
if (!root.visible(link)) continue;
const { img, alt, hasImage, hasImageAlt, imageAltMissing, accessibleName } = getLinkInfo(link);
const hrefRaw = normalize(link.getAttribute('href') || '');
const inTable = Boolean(link.closest('table'));
const issueParts = [];
link.dataset.a11yLink = 'true';
link.dataset.a11yLinkName = accessibleName || '';
link.classList.add('a11y-link-highlight');
link.style.outlineOffset = '2px';
if (hasImage) {
link.dataset.a11yLinkLabel = hasImageAlt ? alt : 'Link image without alt text';
link.classList.add('a11y-link-image-label', 'a11y-link-image-wrap');
link.style.outlineOffset = '2px';
link.style.display = 'inline-block';
link.style.outline = '4px solid #087f23';
if (img) {
img.setAttribute('data-a11y-link-image', 'true');
img.style.outline = '2px solid #087f23';
img.style.outlineOffset = '2px';
img.style.display = 'inline-block';
}
if (!hasImageAlt) link.classList.add('a11y-link-image-missing');
}
if (!hrefRaw) {
issueParts.push('MISSING HREF');
issueFlags.missingHref = true;
}
if (hasImage && imageAltMissing) {
issueParts.push('Link image without alt text');
issueFlags.imageAltMissing = true;
}
if (hrefRaw && !hasImageAlt && accessibleName && sameTextDifferentHref(links, link, accessibleName, hrefRaw)) {
issueParts.push('Duplicated text with different destination');
issueFlags.duplicate = true;
}
if (hrefRaw && accessibleName && isGeneric(accessibleName)) {
issueParts.push('Link text is too generic');
}
if (hrefRaw && !inTable && (link.getAttribute('target') || '').toLowerCase() === '_blank') {
const after = getComputedStyle(link, '::after').content;
if (after === 'none') issueParts.push('Link should clearly indicate that it opens in a new window or tab');
}
if (hrefRaw && isDocumentLink(link)) {
const ext = fileExt(link.href);
if (ext && !normalize(accessibleName || '').toLowerCase().includes(ext)) {
issueParts.push('Document link should include the file format and size');
}
}
const issue = issueParts.join('. ').trim();
if (issue) {
link.setAttribute('data-textlink', issueParts[0] === 'MISSING HREF' && issueParts.length === 1 ? 'MISSING HREF' : issue);
link.classList.add('a11y-link-issue');
link.classList.remove('a11y-link-ok');
if (hasImage) {
link.style.outline = '4px dotted #c00000';
if (img) img.style.outline = '2px dotted #c00000';
}
} else if (accessibleName) {
link.classList.add('a11y-link-ok');
link.style.outline = '4px solid #087f23';
if (hasImage && img) img.style.outline = '2px solid #087f23';
}
if (!link.dataset.a11yLinkHandlers) {
link.dataset.a11yLinkHandlers = 'true';
link.addEventListener('focus', () => link.classList.add('a11y-link-focus'));
link.addEventListener('blur', () => link.classList.remove('a11y-link-focus'));
}
const baseName = accessibleName || (hasImage && imageAltMissing ? 'Link image without alt text' : '(empty)');
const reportName = baseName;
findings.push({
el: link,
accessibleName: baseName,
reportName,
alt,
href: hrefRaw,
issue,
hasImage: Boolean(img),
hasImageAlt,
imageAltMissing,
issueParts
});
}
const html = root.makeSection('Links', `
<ol>
<li>Can the link text be understood on its own, without relying on the content around it?</li>
<li>Does the link text avoid general terms like "Find out more" or "Click here"?</li>
<li>Does the link text accurately describe the destination it goes to?</li>
<li>Do links using the same text go to the same destination?</li>
<li>Do text links use more than just colour, to be distinguishable?</li>
</ol>
<p>All links are listed below by accessible name.</p>
<ol class="a11y-compact-list">
${findings.map((f) => {
const name = root.escapeHtml(f.reportName || f.accessibleName);
const primaryIssue = Array.isArray(f.issueParts) && f.issueParts.length ? f.issueParts[0] : '';
const isWarn = ['MISSING HREF', 'Duplicated text with different destination', 'Link image without alt text'].includes(primaryIssue);
const issueHtml = isWarn ? ` <strong class="warn">- ${root.escapeHtml(primaryIssue)}</strong>` : '';
return `
<li class="a11y-heading-row">
<span class="a11y-heading-flag">LINK</span>
<span class="a11y-heading-copy"><span class="a11y-report-linkname">${name}</span>${issueHtml}</span>
</li>
`;
}).join('')}
</ol>
`);
return { findings, html };
};
})();
(() => {
const root = window.A11Y_EMAIL;
root.renderImageOverlay = () => {};
root.checkImages = () => {
const images = Array.from(document.querySelectorAll('img')).filter(root.visible);
const findings = [];
for (const img of images) {
const role = (img.getAttribute('role') || '').toLowerCase();
const altAttr = img.getAttribute('alt');
const alt = altAttr === null ? null : altAttr.trim();
const noneRole = role === 'presentation' || role === 'none';
const status = noneRole ? 'none' : (alt === null ? 'missing' : (alt === '' ? 'empty' : 'present'));
img.setAttribute('data-a11y-image', 'true');
img.dataset.a11yImageStatus = status;
img.classList.remove('a11y-image-present', 'a11y-image-empty', 'a11y-image-missing');
img.removeAttribute('data-text');
if (status === 'missing') {
img.classList.add('a11y-image-missing');
img.setAttribute('data-text', 'MISSING ALT TEXT');
img.dataset.a11yImageLabel = 'MISSING ALT TEXT';
} else if (status === 'empty') {
img.classList.add('a11y-image-empty');
img.setAttribute('data-text', 'Decorative');
img.dataset.a11yImageLabel = 'Decorative';
} else if (status === 'none') {
img.classList.add('a11y-image-present');
img.setAttribute('data-text', 'None');
img.dataset.a11yImageLabel = 'None';
} else {
img.classList.add('a11y-image-present');
img.dataset.a11yImageLabel = alt;
}
findings.push({
el: img,
alt,
status,
decorative: status === 'empty' || status === 'none',
issue: status === 'missing' ? 'Image alt-text is missing' : ''
});
}
const html = root.makeSection('Images and alt text', `
<p>Please manually check all image alt texts are appropriate, and that decorative images do not add any extra meaning to the content.</p>
<ul>
<li><span class="a11y-key-swatch a11y-key-swatch-green"></span> Outline solid green + alt text - check the alt text is relevant.</li>
<li><span class="a11y-key-swatch a11y-key-swatch-blue"></span> Outline dashed blue + "Decorative" - Check image does not add any further meaning to the content.</li>
<li><span class="a11y-key-swatch a11y-key-swatch-green"></span> Outline solid green + "None" - Image has no role and empty alt text.</li>
<li><span class="a11y-key-swatch a11y-key-swatch-red"></span> Outline dotted red + "MISSING ALT TEXT" - Image requires an alt text.</li>
</ul>
`);
return { findings, html };
};
})();
(() => {
const root = window.A11Y_EMAIL;
root.checkTables = () => {
const tables = Array.from(document.querySelectorAll('table')).filter(root.visible);
const findings = [];
root.clearTableAnnotations();
for (const table of tables) {
const role = (table.getAttribute('role') || '').toLowerCase();
const hasRole = table.hasAttribute('role');
let status = 'ok';
let label = '';
table.dataset.a11yTable = 'true';
table.classList.remove('a11y-table-highlight', 'a11y-table-issue', 'a11y-table-possible-list');
table.removeAttribute('data-text');
if (!hasRole) {
status = 'issue';
label = 'MISSING ROLE';
table.classList.add('a11y-table-issue');
} else if (role === 'presentation' || role === 'none') {
status = 'ok';
label = '';
table.classList.add('a11y-table-highlight');
} else {
status = 'info';
label = role || 'role';
table.classList.add('a11y-table-possible-list');
}
table.dataset.a11yTableStatus = status;
if (label) table.dataset.a11yTableLabel = label; else table.removeAttribute('data-a11y-table-label');
findings.push({ el: table, role, hasRole, status, hasIssues: status === 'issue' });
}
const missingRoles = findings.filter((f) => !f.hasRole).length;
const unexpectedRoles = findings.filter((f) => f.hasRole && f.role && f.role !== 'presentation' && f.role !== 'none');
const unexpectedRoleNames = Array.from(new Set(unexpectedRoles.map((f) => f.role))).join(', ');
const bullets = [];
bullets.push('Tables with light green solid outline are marked correctly as presentational.');
bullets.push('Tables with red dotted outline do not have a role stated.');
bullets.push('It is expected for the outermost table to have a role of "main".');
if (missingRoles) bullets.push('Table(s) found with a MISSING ROLE.');
if (unexpectedRoles.length) bullets.push(`Table(s) found with an unexpected role: ${unexpectedRoleNames}.`);
if (!missingRoles && !unexpectedRoles.length) bullets.push('No issues found.');
const html = root.makeSection('Tables', `
<ul>${bullets.map((b) => `<li>${root.escapeHtml(b)}</li>`).join('')}</ul>
`);
return { findings, html };
};
})();
(() => {
const root = window.A11Y_EMAIL;
const textOf = (el) => (el.textContent || '').replace(/\s+/g, ' ').trim();
const visibleImgs = (el) => Array.from(el.querySelectorAll('img')).filter(root.visible);
const rowCells = (row) => Array.from(row.children).filter((node) => node.matches?.('td,th'));
const directRows = (table) => Array.from(table.children).flatMap((child) => {
if (child.matches?.('tr')) return [child];
if (child.matches?.('tbody,thead,tfoot')) return Array.from(child.children).filter((row) => row.matches?.('tr'));
return [];
}).filter((row) => root.visible(row) && row.closest('table') === table);
const normalizeText = (value) => (value || '').replace(/\s+/g, ' ').replace(/\u00a0/g, ' ').trim();
const cellTextWithoutNestedTables = (cell) => {
const clone = cell.cloneNode(true);
clone.querySelectorAll('table').forEach((table) => table.remove());
clone.querySelectorAll('img,svg,picture,source').forEach((node) => node.remove());
return normalizeText(clone.textContent || '');
};
const cellVisibleImgsInSameTable = (cell, table) => Array.from(cell.querySelectorAll('img')).filter((img) => root.visible(img) && img.closest('table') === table);
const cellIsSpacer = (cell, table) => {
const text = cellTextWithoutNestedTables(cell);
const imgs = cellVisibleImgsInSameTable(cell, table);
const style = `${cell.getAttribute('style') || ''} ${cell.style?.cssText || ''}`;
const widthAttr = parseInt(cell.getAttribute('width') || '0', 10);
const rect = typeof cell.getBoundingClientRect === 'function' ? cell.getBoundingClientRect() : { width: 0, height: 0 };
const tinyWidth = (widthAttr > 0 && widthAttr <= 24) || (rect.width > 0 && rect.width <= 24);
const spacerStyle = /font-size:\s*0|line-height:\s*0|height:\s*(?:0|10|15|20)px/i.test(style);
return (!text && imgs.length === 0) || (tinyWidth && !text) || spacerStyle;
};
const classifyCell = (cell, table) => {
const hasImage = cellVisibleImgsInSameTable(cell, table).length > 0;
const hasText = Boolean(cellTextWithoutNestedTables(cell));
const isSpacer = cellIsSpacer(cell, table);
return { hasImage, hasText, hasContent: hasImage || hasText, isSpacer };
};
const nestedOwnedTable = (row, ownerTable) => {
const candidates = Array.from(row.querySelectorAll('table')).filter((table) => table !== ownerTable && table.closest('table') === ownerTable);
if (candidates.length !== 1) return null;
const cellText = normalizeText(row.textContent || '');
const candidateText = normalizeText(candidates[0].textContent || '');
if (cellText && candidateText && cellText !== candidateText) return null;
return candidates[0];
};
const classifyRowInTable = (row, table) => {
const cells = rowCells(row);
if (!cells.length) return 'spacer-row';
const contentCells = cells
.map((cell) => ({ cell, meta: classifyCell(cell, table) }))
.filter((entry) => entry.meta.hasContent || !entry.meta.isSpacer);
if (!contentCells.length || contentCells.every((entry) => entry.meta.isSpacer || !entry.meta.hasContent)) {
return 'spacer-row';
}
const meaningfulCells = contentCells.filter((entry) => entry.meta.hasContent && !entry.meta.isSpacer);
if (meaningfulCells.length >= 2) {
const first = meaningfulCells[0].meta;
const last = meaningfulCells[meaningfulCells.length - 1].meta;
if (first.hasImage && !first.hasText && !last.hasImage && last.hasText) return 'list-row';
}
const nested = nestedOwnedTable(row, table);
if (nested) {
const nestedRows = directRows(nested);
if (nestedRows.length === 1) {
const nestedType = classifyRowInTable(nestedRows[0], nested);
if (nestedType === 'list-row') return 'list-row';
if (nestedType === 'spacer-row') return 'spacer-row';
}
}
return 'other-row';
};
const detectVisualList = (table) => {
const rows = directRows(table);
let maxRun = 0;
let currentRun = 0;
let found = false;
for (const row of rows) {
const type = classifyRowInTable(row, table);
if (type === 'list-row') {
currentRun += 1;
maxRun = Math.max(maxRun, currentRun);
found = true;
continue;
}
if (type === 'spacer-row') {
continue;
}
currentRun = 0;
}
return maxRun;
};
const analyseCoding = (table) => {
const tbody = table.querySelector(':scope > tbody');
if (!tbody) return { hasListRole: false, correct: false, partial: false, itemCount: 0 };
const tbodyRole = (tbody.getAttribute('role') || '').toLowerCase();
if (tbodyRole !== 'list') return { hasListRole: false, correct: false, partial: false, itemCount: 0 };
const rows = Array.from(tbody.querySelectorAll(':scope > tr')).filter((row) => root.visible(row) && row.closest('table') === table);
let itemCount = 0;
let partial = false;
for (const row of rows) {
const role = (row.getAttribute('role') || '').toLowerCase();
const hidden = row.getAttribute('aria-hidden') === 'true';
const spacer = !textOf(row) && visibleImgs(row).length === 0;
if (role === 'listitem') {
itemCount += 1;
continue;
}
if (spacer && hidden) continue;
partial = true;
}
const correct = !partial && itemCount >= 2;
if (itemCount < 2) partial = true;
return { hasListRole: true, correct, partial, itemCount };
};
root.checkVisualLists = () => {
const tables = Array.from(document.querySelectorAll('table')).filter(root.visible);
const findings = [];
let foundVisualLists = false;
let issues = false;
for (const table of tables) {
const visualCount = detectVisualList(table);
const coding = analyseCoding(table);
const isVisualList = visualCount >= 2;
if (!isVisualList && !coding.hasListRole) continue;
foundVisualLists = foundVisualLists || isVisualList || coding.hasListRole;
table.dataset.a11yTable = 'true';
table.dataset.a11yTableLabel = isVisualList ? 'Possible visual list' : 'list';
const hasIssue = (isVisualList && !coding.correct) || coding.partial || (coding.hasListRole && coding.itemCount < 2);
if (hasIssue) {
issues = true;
table.dataset.a11yTableStatus = 'issue';
table.classList.add('a11y-table-issue');
table.classList.remove('a11y-table-highlight', 'a11y-table-possible-list');
table.setAttribute('data-text', 'List coding issue');
} else {
table.dataset.a11yTableStatus = isVisualList ? 'info' : 'ok';
table.classList.remove('a11y-table-issue');
table.removeAttribute('data-text');
table.classList.add(isVisualList ? 'a11y-table-possible-list' : 'a11y-table-highlight');
}
findings.push({ el: table, isVisualList, hasIssue, coding });
}
const html = root.makeSection('Visual lists', `
<ul>
<li>${issues ? 'Issues found.' : 'No issues found.'}</li>
<li>${foundVisualLists ? 'List(s) found which require visual conformation.' : 'A visual check is in order.'}</li>
</ul>
`);
return { findings, html };
};
})();
(() => {
const root = window.A11Y_EMAIL;
const selector = ':is(a[href],area[href],audio[controls],button,embed[src],iframe,input,select,summary,textarea,video,[contenteditable],[tabindex]):not([hidden],:disabled,[tabindex^="-"])';
const radioTarget = (name) => {
if (!name) return null;
const radios = Array.from(document.querySelectorAll(`input[type="radio"][name="${name.replaceAll('"', '\\"')}"]`));
if (!radios.length) return null;
return radios.find((r) => r.checked) || radios[0];
};
const isEmailLikePage = () => {
const tableCount = document.querySelectorAll('table').length;
return tableCount >= 4 || Boolean(document.querySelector('[aria-label="Santander email"], [aria-label="Santander"]'));
};
root.renderTabStopsOverlay = (findings) => {
root.removeById(root.ids.svg);
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.id = root.ids.svg;
svg.setAttribute('aria-hidden', 'true');
Object.assign(svg.style, {
position: 'absolute',
inset: '0',
overflow: 'visible',
zIndex: '10000',
pointerEvents: 'none',
margin: '0'
});
const make = (tag) => document.createElementNS('http://www.w3.org/2000/svg', tag);
const lineGroup = make('g');
const markerGroup = make('g');
const placed = [];
for (const finding of findings) {
const rect = finding.el.getBoundingClientRect();
const x = Math.round(rect.left + rect.width / 2 + window.scrollX);
const y = Math.round(rect.top + rect.height / 2 + window.scrollY);
placed.push({ finding, x, y });
}
for (let i = 1; i < placed.length; i += 1) {
const prev = placed[i - 1];
const curr = placed[i];
const line = make('line');
line.setAttribute('x1', String(prev.x));
line.setAttribute('y1', String(prev.y));
line.setAttribute('x2', String(curr.x));
line.setAttribute('y2', String(curr.y));
line.setAttribute('class', 'line');
lineGroup.append(line);
}
for (const { finding, x, y } of placed) {
const rectMarker = make('rect');
rectMarker.setAttribute('x', String(x - 12));
rectMarker.setAttribute('y', String(y - 12));
rectMarker.setAttribute('width', '24');
rectMarker.setAttribute('height', '24');
markerGroup.append(rectMarker);
const text = make('text');
text.setAttribute('x', String(x));
text.setAttribute('y', String(y));
text.setAttribute('text-anchor', 'middle');
text.setAttribute('dominant-baseline', 'central');
text.textContent = String(finding.order);
markerGroup.append(text);
if (!finding.el.dataset.a11yTabstopHandlers) {
finding.el.dataset.a11yTabstopHandlers = 'true';
finding.el.addEventListener('focus', () => rectMarker.classList.add('-js-focused'));
finding.el.addEventListener('blur', () => rectMarker.classList.remove('-js-focused'));
}
}
svg.append(lineGroup);
svg.append(markerGroup);
root.ensureBody().appendChild(svg);
};
root.checkTabStops = () => {
const candidates = Array.from(document.querySelectorAll(selector));
const usedRadioGroups = new Set();
const findings = [];
let order = 0;
for (const el of candidates) {
if (!root.visible(el)) continue;
let target = el;
if (el instanceof HTMLInputElement && el.type === 'radio') {
const name = el.name || '';
if (!name || usedRadioGroups.has(name)) continue;
const chosen = radioTarget(name);
if (!chosen || !root.visible(chosen)) continue;
usedRadioGroups.add(name);
target = chosen;
}
target.classList.add('-js-tabable');
order += 1;
findings.push({ el: target, order });
}
const isEmail = isEmailLikePage();
const tabindexNodes = isEmail
? Array.from(document.querySelectorAll('[tabindex]')).filter((el) => root.visible(el) && String(el.getAttribute('tabindex') || '').trim() !== '')
: [];
for (const el of tabindexNodes) {
el.classList.add('a11y-link-issue');
el.style.outline = '2px dotted #c00000';
el.style.outlineOffset = '2px';
el.setAttribute('data-text', `tabindex=${el.getAttribute('tabindex')}`);
}
const html = [
root.makeSection('Keyboard tab-stops', `
<p>Using only the keyboard, navigate through the document using <kbd>Tab</kbd> and <kbd>Shift</kbd> + <kbd>Tab</kbd>, or the arrow keys on radio buttons. Activate links by pressing <kbd>Return</kbd>, and buttons with <kbd>Return</kbd> or <kbd>Spacebar</kbd>.</p>
<p>Reporting is state dependant, remember to <strong>expand accordions</strong> before testing.</p>
${isEmail ? `<p class="muted">- ${tabindexNodes.length ? `Tabindex found in email: ${root.escapeHtml(String(tabindexNodes.length))}. There should be none.` : 'No tabindex found in email.'}</p>` : ''}
<ol>
<li>When an element is focused by keyboard, is your current position clearly indicated?</li>
<li>Are all interactive elements reachable and usable with just the keyboard?</li>
<li>Does keyboard focus stop on non-interactive elements?</li>
<li>Is any tab stop completely obscured by the overlaid yellow square marker?</li>
<li>Does the keyboard tab order proceed in a logical sequence from left to right and top to bottom?</li>
<li>Are there any points where the keyboard becomes stuck?</li>
<li>Are there any elements which remain invisible upon keyboard focus?</li>
</ol>
`)
].join('');
return { findings, html };
};
})();
(() => {
const root = window.A11Y_EMAIL;
root.applyTextSpacing = () => {
root.injectStyles(`
.a11y-textspacing, .a11y-textspacing *{line-height:1.5 !important;letter-spacing:0.12em !important;word-spacing:0.16em !important}
font-family:inherit !important;font-size:inherit !important;font-weight:inherit;font-style:inherit}
.a11y-textspacing p,.a11y-textspacing li,.a11y-textspacing td,.a11y-textspacing th,.a11y-textspacing a,.a11y-textspacing span,.a11y-textspacing div{word-break:normal;overflow-wrap:anywhere}
#a11y-email-console-ui.a11y-textspacing-exempt, #a11y-email-console-ui.a11y-textspacing-exempt *{line-height:normal !important;letter-spacing:normal !important;word-spacing:normal !important}
`);
root.ensureBody().classList.add('a11y-textspacing');
document.getElementById('a11y-email-console-ui')?.classList.add('a11y-textspacing-exempt');
const html = root.makeSection('Maximum text spacing', `
<p>The page has been temporarily styled with WCAG text-spacing values.</p>
<ul>
<li><code>line-height: 1.5</code></li>
<li><code>letter-spacing: 0.12em</code></li>
<li><code>word-spacing: 0.16em</code></li>
</ul>
<p class="muted">Review the page visually for clipping, overlap, and reflow issues.</p>
`);
return { findings: [], html };
};
root.clearTextSpacing = () => {
root.ensureBody().classList.remove('a11y-textspacing');
document.getElementById('a11y-email-console-ui')?.classList.remove('a11y-textspacing-exempt');
};
})();
(() => {
const root = window.A11Y_EMAIL;
function clearAll() {
root.clearAll();
root.clearTextSpacing?.();
}
function renderResults(parts) {
const html = [
`<p class="a11y-report-intro">For each check, read and answer the questions shown.</p>`,
...parts
].join('');
root.toggleReport(html);
root.injectStyles(root.reportShellCss);
}
function runChecks(settings = {}) {
clearAll();
const merged = { ...root.defaults, ...settings };
if (!Object.entries(merged).some(([k,v]) => k !== 'baseFontSize' && v === true)) {
root.toggleReport('');
root.injectStyles(root.reportShellCss);
return { settings: merged, sectionsRendered: 0, clear: clearAll };
}
if (merged.tableLinearise) root.setTableLinearise(true);
const sections = [];
let tabStopFindings = [];
let largeTextRes = null;
if (merged.headings) {
largeTextRes = root.checkLargeText(merged.baseFontSize);
sections.push(root.checkHeadings({ largeTextFindings: largeTextRes ? largeTextRes.findings : [] }).html);
}
if (merged.links) sections.push(root.checkLinks().html);
if (merged.tabStops) {
const res = root.checkTabStops();
sections.push(res.html);
tabStopFindings = res.findings;
}
if (merged.emailMeta) sections.push(root.checkEmailMeta().html);
if (merged.images) sections.push(root.checkImages().html);
if (merged.tables) sections.push(root.checkTables({ linearise: false }).html);
if (merged.tableLinearise) sections.push(root.makeSection('Linearised tables', `<p>- All table cells are linearised into a single column. Please check that the content order still makes sense.</p>`));
if (merged.textSpacing) sections.push(root.applyTextSpacing().html);
renderResults(sections);
root.renderGeometryOverlay?.();
if (merged.tabStops) root.renderTabStopsOverlay(tabStopFindings);
return {
settings: merged,
sectionsRendered: sections.length,
clear: clearAll
};
}
function currentSettingsFromUi() {
const panel = document.getElementById('a11y-email-console-ui');
if (!panel) return { ...root.defaults };
const get = (name) => panel.querySelector(`[name="${name}"]`);
return {
headings: !!get('headings')?.checked,
links: !!get('links')?.checked,
tabStops: !!get('tabStops')?.checked,
emailMeta: !!get('emailMeta')?.checked,
images: !!get('images')?.checked,
tables: !!get('tables')?.checked,
tableLinearise: !!get('tableLinearise')?.checked,
textSpacing: !!get('textSpacing')?.checked,
lists: !!get('lists')?.checked,
baseFontSize: Number.parseFloat(get('baseFontSize')?.value || '16') || 16
};
}
function buildConsoleUi() {
document.getElementById('a11y-email-console-ui')?.remove();
document.getElementById('a11y-email-console-ui-style')?.remove();
const panel = document.createElement('aside');
panel.id = 'a11y-email-console-ui';
panel.innerHTML = `
<div class="a11y-console-card">
<div class="a11y-console-head" data-drag-handle="true">
<strong>Email accessibility testing suite</strong>
</div>
<p class="a11y-console-copy">Tick a check to turn it on. Untick all checks to clear the overlay.</p>
<div class="a11y-console-grid">
<div class="a11y-console-list">
<div class="a11y-console-subhead">Squadies</div>
<label><input type="checkbox" name="tableLinearise"> Linearise tables</label>
<label><input type="checkbox" name="images"> Image alts</label>
<label><input type="checkbox" name="links"> Links</label>
<label><input type="checkbox" name="headings"> Headings and large text</label>
</div>
<div class="a11y-console-list">
<div class="a11y-console-subhead">Developers</div>
<label><input type="checkbox" name="emailMeta"> Email shell</label>
<label><input type="checkbox" name="tables"> Tables</label>
<label><input type="checkbox" name="tabStops"> Keyboard tab-stops</label>
<label><input type="checkbox" name="textSpacing"> Text spacing</label>
</div>
<label class="a11y-console-font">Base font size <input type="number" min="1" step="1" name="baseFontSize" value="16"></label>
</div>
<div class="a11y-console-divider" aria-hidden="true"></div>
</div>
<output id="${root.ids.report}" class="a11y-report" role="region" aria-label="Accessibility report"></output>
`;
document.body.appendChild(panel);
const style = document.createElement('style');
style.id = 'a11y-email-console-ui-style';
style.textContent = `
#a11y-email-console-ui{position:fixed;top:16px;right:16px;left:auto;z-index:2147483647;font:400 14px/1.4 Arial,sans-serif;color:#222;width:440px;max-width:min(440px,calc(100vw - 32px));height:calc(100vh - 32px);display:flex;flex-direction:column;user-select:text;background:#fff;border:2px solid #ccc;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);overflow:hidden}
#a11y-email-console-ui .a11y-console-card{background:#fff;border:0;border-radius:0;box-shadow:none;padding:12px 12px 8px;flex:none}
#a11y-email-console-ui .a11y-console-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;cursor:move;user-select:none}
#a11y-email-console-ui .a11y-console-copy{margin:0 0 24px}
#a11y-email-console-ui .a11y-console-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px 16px;align-items:start}
#a11y-email-console-ui .a11y-console-list{display:grid;gap:8px;align-content:start}.a11y-console-subhead{font-weight:700;margin:0 0 6px}
#a11y-email-console-ui label{display:flex;align-items:center;gap:8px;min-width:0}
#a11y-email-console-ui .a11y-console-font{justify-content:space-between;grid-column:1 / -1;margin-top:2px}
#a11y-email-console-ui input[type="number"]{width:72px;padding:4px}
#a11y-email-console-ui .a11y-console-divider{margin-top:10px;border-top:1px solid #d6d6d6}
#${root.ids.report}{display:block;background:transparent !important;box-shadow:none;border:0 !important;border-radius:0;box-sizing:border-box;padding:0 !important;margin:0;flex:1;min-height:0;overflow:auto;font:400 12px/1.45 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#222;user-select:text}
#${root.ids.report} .a11y-report-intro{margin:40px 0 24px;padding:0;color:#333}
#${root.ids.report} h2{text-align:left;font:600 17px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:48px 0 24px;padding:0}
#${root.ids.report} h3{text-align:left;font:600 14px/1.3 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:36px 0 20px;padding:0}
#${root.ids.report} p,#${root.ids.report} ol,#${root.ids.report} ul,#${root.ids.report} table,#${root.ids.report} .a11y-section>div{padding:0}
#${root.ids.report} ol,#${root.ids.report} ul{margin:8px 0 0 20px}
#${root.ids.report} .indent{margin-left:0 !important;padding-left:0 !important;list-style:none}
#${root.ids.report} ol.a11y-compact-list,#${root.ids.report} ul.a11y-compact-list{margin:8px 0 0 0;padding:0;list-style:none}
#${root.ids.report} li{margin:4px 0}
#${root.ids.report} code,#${root.ids.report} kbd{background:#fff;border:1px solid #ccc;border-radius:4px;padding:0 .3em}
#${root.ids.report} .warn{color:#c00000;font-weight:600}
#${root.ids.report} .ok{color:#087f23;font-weight:600}
#${root.ids.report} .muted{color:#555}
#${root.ids.report} table{border-collapse:collapse;width:100%;margin:0}
#${root.ids.report} td,#${root.ids.report} th{border:1px solid #ddd;padding:4px 6px;vertical-align:top}
#${root.ids.report} .a11y-section{margin-bottom:16px}
#${root.ids.report} .a11y-status-pill{display:inline-flex;align-items:center;justify-content:center;min-width:2.4em;padding:2px 8px;border-radius:999px;background:#666;color:#fff;font:700 12px/1 system-ui;white-space:nowrap;flex:none}
#${root.ids.report} .a11y-status-pill.info,#${root.ids.report} .a11y-large-size-pill{background:#0057d9 !important;color:#fff !important}
#${root.ids.report} .a11y-large-prompt{color:#0057d9;font-weight:500}
#${root.ids.report} .a11y-report-linkname{color:#0b5e20 !important;text-decoration:underline !important;font-weight:400 !important}
`;
document.head.appendChild(style);
const sync = () => {
const settings = currentSettingsFromUi();
const enabled = Object.entries(settings).some(([k,v]) => k !== 'baseFontSize' && v === true);
if (enabled) {
runChecks(settings);
} else {
clearAll();
const report = document.getElementById(root.ids.report);
if (report) report.innerHTML = '';
root.injectStyles(root.reportShellCss);
}
};
panel.addEventListener('change', (event) => {
const target = event.target;
if (!(target instanceof HTMLInputElement)) return;
if (target.type === 'checkbox' || target.name === 'baseFontSize') sync();
});
const handle = panel.querySelector('[data-drag-handle="true"]');
let dragging = false;
let startX = 0;
let startY = 0;
let startLeft = 16;
let startTop = 16;
const rectWidth = (el) => el.getBoundingClientRect().width;
const rectHeight = (el) => el.getBoundingClientRect().height;
const onMove = (event) => {
if (!dragging) return;
const dx = event.clientX - startX;
const dy = event.clientY - startY;
panel.style.left = `${Math.max(8, Math.min(window.innerWidth - rectWidth(panel) - 8, startLeft + dx))}px`;
panel.style.top = `${Math.max(8, Math.min(window.innerHeight - rectHeight(panel) - 8, startTop + dy))}px`;
};
const onUp = () => {
dragging = false;
document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerup', onUp);
};
handle?.addEventListener('pointerdown', (event) => {
dragging = true;
startX = event.clientX;
startY = event.clientY;
const rect = panel.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
panel.style.right = 'auto';
document.addEventListener('pointermove', onMove);
document.addEventListener('pointerup', onUp);
});
return panel;
}
window.A11Y_EMAIL_CONSOLE = {
run: runChecks,
clear: clearAll,
ui: buildConsoleUi,
defaults: { ...root.defaults },
root
};
buildConsoleUi();
})();
How to use the snippet
- Copy the minified console snippet.
- Open the web or online version of the email you want to test.
- Open Chrome Developer Tools.
You can do this by pressing:- F12
- Windows: Ctrl + Shift + I
- Mac: Cmd + Option + I
- You can also right click and choose "Inspect".
-
Select the tab called "Console"".
Developer Tools may open on a different tab first. - Click in the Console input area, paste the code, then press Enter
The testing panel and overlay should then appear on the page. - Use the controls shown by the snippet to turn checks on or off.
Practice emails
A couple of example emails to try your testing skills:
Version and security
Both the console snippet and bookmarklets presented here are completely self-contained, and do not reference any external resources, which means they're safe to use where security restrictions are tight.
All versions may contain some obscure minor bugs. For any issues encountered please email Mike Foskett for support.
If anything is unclear, please contact me.
Older bookmarklets
For most testing, use the console snippet above.
Bookmarklet version (2025)
Email testing tool
New for 2025, a single bookmarklet:
All the previously used bookmarklets (archived below) have been updated and collated into a single bookmarklet. The old bookmarklet "Links & tab-stops" has been split into two for clarity.
Using the bookmarklet
- Add bookmarklet to the browsers bookmarks (dragging it usually works).
- Navigate to the online version of the email for testing.
- Click the bookmark.
Older individual bookmarklets
Older individual bookmarklets
Kept until the new version has been battle-tested against real emails.
There is no specific order to run the checks, but please refresh the page before each check. Sometimes running two at the same time can help with clarity. For example, "Linearise tables" and "Image alt text".
-
Outline tables
Bookmarklet:
Tables check v2.0 Outlines all the tables in the email so you can see the presence of presentation tables.
- Solid green outline
- valid for layout purposes - Dotted red outline
- must be a comparision table. Please check this is the case.
Helps to check:
- Solid green outline
-
Linearise tables
Bookmarklet:
Layout check v1.4 Check the layout table structure doesn't change the reading order of the email.
Upon clicking, layout table cells are put into a single column, so they may be read in the same order that a screen-reader uses. The layout table is outlined:
- Solid faint green outlines - Linearised table, check the reading order is still okay.
Helps to check:
-
Image alt text
Bookmarklet:
Image alt check v5.2 All images show their alt text.
- Solid green outline
Good, but check the alt text is relevant to the content. - Dashed blue outline
Image is inside a link, check the alt text describes the destination. - Dotted red outline
No alt text present, remedial action required.
Version 5 introduces a test for the Outlook component
v:roundrectand checks for an alt text, reporting when missing.Helps to check:
- Solid green outline
-
Outline headings
Bookmarklet:
Headings check v3.5 Check all the headings are marked and are at the correct level. Check for anything missed or inappropriate.
- One h1 per email, the emails main heading
- Sub-sections are h2
- Sub-sub-sections are h3
- Sub-sub-sub-sections are h4
Test it on this page to see what it does.
Upon clicking, check all headings are outlined:
- Solid green outlines - Headings h1 to h6.
- Dotted red outlines
- Empty headings, please remove.
- Or, a missing H1 heading and remediation is required.
- Or, more than a single H1 heading.
Helps to check:
-
Outline large text
Bookmarklet:
Large text check v3.65 Text which is larger than 14px (body copy) will be highlighted with a dotted red outline. This test ignores headings, links, and elements without textual copy. A left-hand purple border is applied to indicate the bookmarklet is running.
Please sense check anything highlighted, should it be a heading?
Helps to check:
-
Highlights links and keyboard tab stops
Bookmarklet:
Links & tab-stops v2.0 - Solid green outlines - A valid link. Check the link text makes sense and you're good to go.
- Dotted red outlines - An invalid link, a tabindexed element, or both. Requires remediation.
Becomes a dashed blue outline upon keyboard focus.
Helps to check:
-
Text spacing
Bookmarklet:
Text spacing check v2.0 Check increased text spacing is available without cropping.
Upon clicking, the text is spaced out to meet the maximum recommended. Ensure text doesn't appear cropped.
Helps to check: