Are you ready to assess your digital accessibility?

Before the external audit begins, we ask that you complete the preliminary self-assessment form below, save it as a PDF, then email it to tom.paget@santander.co.uk.

Unless cleared, data is preserved locally on your device from visit to visit. For security, this data is never uploaded to a server.

Self-assessment - Digital Accessibility Audit

Assessor: Email: Dated:

Scope of assessment

Type of document:

Assessment synopsis

Assessment summary by section
SectionQuestionsPassedFailedNot ApplicablePass/Fail
Headings4000
Links7000
Keyboard navigation7000
Text resize4000
Automated testing5000
Total questions27Unanswered27
Type of document

To assess desktop views, we recommend using Google Chrome with the provided console snippet, and Lighthouse or Deque Axe DevTool as an automated testing tool.

For PDFs, and other documents, simply answering the provided questions should be sufficient.

Wherever possible try to run these checks across different hardware (MacOS, Windows, iOS, Android) and browser (Chrome, Firefox, Edge, Safari) combinations.

When testing mobile apps, we recommend also using a screen reader, such as VoiceOver on iOS or Talkback on Android.

The manual accessible checks

Console snippet test tool (Update June 2026)

The testing bookmarklets have been superseded by a console snippet which works with any browser.

This opens a small testing panel on the page so you can turn checks on and off.

Minified console snippet

!function(){const t="__THREE_STEP_SELF_AUDIT__",e="tsa-root",n="tsa-report",i="tsa-style",a="tsa-tabstops-svg";window[t]&&window[t].destroy&&window[t].destroy();const o={options:{headings:!1,links:!1,tabStops:!1},cleanupFns:[],focusHandlers:[],observer:null};function s(t,e=document){return Array.from(e.querySelectorAll(t))}function r(t){return!(!t||!(t.closest("#"+e)||t.closest("#"+n)||t.closest("#"+a)))}function l(t){if(!t||!t.isConnected||r(t))return!1;if("function"==typeof t.checkVisibility)try{if(!t.checkVisibility())return!1}catch(t){}const e=getComputedStyle(t);if("none"===e.display||"hidden"===e.visibility||"0"===e.opacity)return!1;const n=t.getBoundingClientRect();return n.width>0&&n.height>0}function d(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function c(){document.documentElement.removeAttribute("data-tsa-headings-active"),document.documentElement.removeAttribute("data-tsa-links-active"),document.documentElement.removeAttribute("data-tsa-tabstops-active"),s("[data-tsa-heading-note],[data-tsa-link-note],[data-tsa-tabstop]").forEach(t=>{t.removeAttribute("data-tsa-heading-note"),t.removeAttribute("data-tsa-link-note"),t.removeAttribute("data-tsa-tabstop"),t.classList.remove("tsa-tabable")}),o.focusHandlers.forEach(({el:t,onFocus:e,onBlur:n})=>{t.removeEventListener("focus",e),t.removeEventListener("blur",n)}),o.focusHandlers=[],o.cleanupFns.forEach(t=>{try{t()}catch(t){}}),o.cleanupFns=[];const t=document.getElementById(a);t&&t.remove();const i=document.getElementById(e),r=i?i.querySelector("#"+n):null;r&&(r.innerHTML=""),i&&i.classList.remove("tsa-has-report")}function p(){let t=document.getElementById(e);if(t)return t;!function(){const t=document.getElementById(i);if(t)return t;const o=document.createElement("style");o.id=i,o.textContent=`\n#${e}{position:fixed;top:16px;left:16px;z-index:2147483646;background:#f7f7f7;border:2px solid #ccc;padding:12px 14px;font:400 14px/1.4 Arial,sans-serif;color:#222;width:320px;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);display:flex;flex-direction:column;gap:0;box-shadow:0 4px 14px rgba(0,0,0,.18);box-sizing:border-box}\n#${e} h2{margin:0 0 8px;font:700 16px/1.2 Arial,sans-serif;cursor:move}\n#${e} fieldset{border:0;padding:0;margin:8px 0 10px}\n#${e} label{display:flex;gap:8px;align-items:center;margin:6px 0}\n#${e} .tsa-controls{flex:0 0 auto}\n#${e} .tsa-help{margin:0 0 8px;color:#444;font-size:12px}\n#${n}{display:none;flex:1 1 auto;min-height:0;overflow:auto;background:#f7f7f7;border:0;border-top:1px solid #ccc;padding:4px 0 0;font:400 13px/1.4 Arial,sans-serif;color:#333;box-shadow:none;box-sizing:border-box;margin-top:8px}\n#${e}.tsa-has-report #${n}{display:block}\n#${n} h2{margin:0 0 8px;font:700 20px/1.2 Arial,sans-serif}\n#${n} h3{margin:28px 0 8px;font:700 17px/1.2 Arial,sans-serif}\n#${n} .tsa-report-help{margin:10px 0 10px;color:#333;font-size:12px}\n#${n} > :first-child h3,#${n} > h3:first-child{margin-top:0}\n#${n} ol, #${n} ul{margin:.7rem 0;padding-left:1rem}\n#${n} .tsa-indent{list-style:none;padding-left:0}\n#${n} .tsa-indent li{margin-top:6px}\n#${n} .tsa-tag{display:inline-block;background:green;color:#fff;padding:2px 4px 0;text-transform:uppercase;margin-right:6px}\n#${n} .tsa-error{color:#c00;font-weight:700}\n#${n} .tsa-img{color:#070}\n#${n} a,#${n} a:visited{color:#1b5e20!important;text-decoration:underline!important;text-underline-offset:2px;font-weight:500}\n#${n} .H1{padding-left:0} #${n} .H2{padding-left:24px} #${n} .H3{padding-left:48px} #${n} .H4{padding-left:72px} #${n} .H5{padding-left:96px} #${n} .H6{padding-left:120px}\nhtml[data-tsa-headings-active="true"] :is(h1,h2,h3,h4,h5,h6,[role="heading"]):not(#${e} *, #${n} *){--tsa-h-color:#000;--tsa-h-bg:#0c0;outline:2px solid var(--tsa-h-bg)!important;outline-offset:-2px;position:relative}\nhtml[data-tsa-headings-active="true"] :is(h1,h2,h3,h4,h5,h6,[role="heading"]):not(#${e} *, #${n} *)::after{content:attr(data-tsa-heading-note);color:var(--tsa-h-color)!important;background:var(--tsa-h-bg)!important;text-shadow:0 0 1px #000;box-sizing:border-box;z-index:2147483640;display:flex;align-items:center;position:absolute;inset:0 0 auto auto;padding:2px 6px;font:500 14px/1.3 Arial,sans-serif;max-width:55%;min-height:100%}\nhtml[data-tsa-headings-active="true"] [data-tsa-heading-note][data-tsa-heading-error="true"]{--tsa-h-color:#fff;--tsa-h-bg:#e50000;outline:4px dotted var(--tsa-h-bg)!important;min-height:1.5rem}\nhtml[data-tsa-links-active="true"] a[href]:not(#${e} *, #${n} *){outline:4px solid green!important;outline-offset:2px!important}\nhtml[data-tsa-links-active="true"] a[href][data-tsa-link-note]:not(#${e} *, #${n} *){outline:4px dotted #c00!important;position:relative;display:inline-block}\nhtml[data-tsa-links-active="true"] a[href][data-tsa-link-note]:not(#${e} *, #${n} *)::after{content:attr(data-tsa-link-note);position:absolute;left:100%;top:0;margin-left:8px;background:#c00;color:#fff;padding:2px 6px;font:500 14px/1.3 Arial,sans-serif;text-shadow:0 0 1px #000;z-index:2147483640;box-sizing:border-box;display:flex;align-items:center;min-height:100%;width:max-content}\nhtml[data-tsa-links-active="true"] a[href]:focus,html[data-tsa-links-active="true"] a[href]:focus-visible{outline-style:dashed!important}\n#${a}{position:absolute;inset:0;overflow:visible;pointer-events:none;z-index:2147483644;margin:0}\n#${a} line{stroke:yellow;stroke-width:4px}\n#${a} rect{fill:yellow;stroke:black;stroke-width:1px}\n#${a} text{fill:black;font-size:10px;font-weight:700}\n#${a} rect.-js-focussed{fill:red!important}\n`,document.head.appendChild(o)}(),t=document.createElement("div"),t.id=e,t.innerHTML=`\n      <div class="tsa-controls">\n        <h2>Accessibility self-audit</h2>\n        <p class="tsa-help">Tick a check to turn it on. Untick all checks to clear the overlay.</p>\n        <fieldset>\n          <label><input type="checkbox" data-opt="headings"> Headings</label>\n          <label><input type="checkbox" data-opt="links"> Links</label>\n          <label><input type="checkbox" data-opt="tabStops"> Tab-stops</label>\n        </fieldset>\n      </div>\n      <output id="${n}" class="reportWindow"></output>\n    `,t.addEventListener("change",t=>{const e=t.target;e&&e.matches("[data-opt]")&&(o.options[e.getAttribute("data-opt")]=e.checked,b())}),document.body.appendChild(t);const s=t.querySelector("h2");let r=null;const l=e=>{if(!r)return;const n=Math.max(0,window.innerWidth-t.offsetWidth),i=Math.max(0,window.innerHeight-t.offsetHeight),a=Math.min(n,Math.max(0,e.clientX-r.dx)),o=Math.min(i,Math.max(0,e.clientY-r.dy));t.style.left=a+"px",t.style.top=o+"px",t.style.right="auto"},d=()=>{r=null,document.removeEventListener("pointermove",l),document.removeEventListener("pointerup",d)};return s.addEventListener("pointerdown",e=>{if(e.target.closest("button,input,label"))return;const n=t.getBoundingClientRect();r={dx:e.clientX-n.left,dy:e.clientY-n.top},document.addEventListener("pointermove",l),document.addEventListener("pointerup",d)}),t}function u(t){const e=t.getAttribute("aria-level");if(e)return parseInt(e,10)||6;const n=t.tagName.toUpperCase().match(/^H([1-6])$/);return n?parseInt(n[1],10):6}function h(t){let e=(t.textContent||"").trim();const n=t.querySelector("img");return n&&(e+=` "${n.getAttribute("alt")||""}"`),[e.trim(),!!n]}function m(t){document.documentElement.setAttribute("data-tsa-links-active","true");const e=document.createElement("div");let n='<h3>Links</h3><ol style="margin:1rem 0"><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 "More details"?</li><li>Does the link text accurately describe the destination it goes to?</li><li>Do links with the same text go to the same destination?</li><li>Do links use additional methods, apart from colour, to be distinguishable?</li><li>Are links that open in a new window or tab clearly indicated?</li><li>Are document links clearly marked with format and file size?</li></ol><ol class="tsa-indent">';const i=s("a[href]").filter(l);i.forEach(t=>t.removeAttribute("data-tsa-link-note")),i.forEach(t=>{let e="";const[a,o]=h(t);let s=t.getAttribute("href")||"";if(s&&s.startsWith("javascript:")&&(s="javascript:"),s){(function(t,e,n,i){return t.some(t=>t!==e&&l(t)&&h(t)[0]===n&&t.getAttribute("href")!==i)})(i,t,a,s)&&(e+="Duplicated link copy, with a different address. ");const n=t.querySelector("img");if(n){const i=n.getAttribute("alt")||"";((t.textContent||"")+i).trim().length||(e+="Link image missing alt text. ")}if(function(t){return["see all","see more","view all","view more","read more","more","click here","here","find out more"].includes((t.textContent||"").trim().toLowerCase())}(t)&&(e+="Does this link text describe the destination? "),function(t){return"_blank"===(t.getAttribute("target")||"").toLowerCase()&&"none"===getComputedStyle(t,"::after").content}(t)&&(e+="Is this link marked as opening in a new window or tab? "),function(t){return!t.href.startsWith("javascript:")&&[".pdf",".xlsx",".docx",".pptx"].some(e=>t.href.includes(e))}(t)){const n=new URL(t.href);n.search="",n.hash="";const i=n.toString(),a=i.indexOf(".")>0?i.split(".").pop().toLowerCase():"undefined";(t.textContent||"").toLowerCase().includes(a)||(e+="Is this document link marked with format and file-size? ")}}else""===s&&(e+="Link missing address. ");e=e.trim(),e&&t.setAttribute("data-tsa-link-note",e),n+=`<li>${o?'<span class="tsa-img">alt-text: </span>':""}<a ${s?`href="${d(s)}"`:""} class="copy">${d(a)}</a> ${e?`<strong class="tsa-error">${d(e)}</strong>`:""}</li>`}),n+="</ol><p>Links in the content are highlighted: valid links are marked with a 4px solid green outline; invalid links are marked with a 4px dotted red outline; the outline becomes dashed on focus.</p>",e.innerHTML=n,t.appendChild(e)}function g(t){const e=s(`input[type="radio"][name="${CSS.escape(t)}"]`).filter(l).filter(t=>!r(t));return e.length?e.find(t=>t.checked)||e[0]:null}function f(t){document.documentElement.setAttribute("data-tsa-tabstops-active","true");const e=document.createElement("div");e.innerHTML="<h3>Keyboard tab-stops</h3><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 <b>expand accordions</b> before testing.</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—specifically 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>",t.appendChild(e);let n=document.getElementById(a);n&&n.remove(),n=document.createElementNS("http://www.w3.org/2000/svg","svg"),n.id=a,document.body.appendChild(n);const i=s(':is(a[href],area[href],audio[controls],button,embed[src],iframe,input,select,summary,textarea,video,[contenteditable],[tabindex]):not([hidden],:disabled,[tabindex^="-"])').filter(l);let r=null,d=0,c="";for(const t of i){if(t.name&&t.name===c)continue;let e=t;if("radio"===t.type&&(e=g(t.name),c=t.name,!e))continue;e.classList.add("tsa-tabable"),e.setAttribute("data-tsa-tabstop","true");const i=e.getBoundingClientRect(),a=Math.round(i.left+i.width/2),s=Math.round(window.scrollY+i.top+i.height/2);if(r){const t=document.createElementNS("http://www.w3.org/2000/svg","line");t.setAttribute("x1",String(r.x)),t.setAttribute("y1",String(r.y)),t.setAttribute("x2",String(a)),t.setAttribute("y2",String(s)),n.appendChild(t)}d+=1;const l=document.createElementNS("http://www.w3.org/2000/svg","rect");l.setAttribute("x",String(a-11)),l.setAttribute("y",String(s-11)),l.setAttribute("width","22"),l.setAttribute("height","22"),n.appendChild(l);const p=document.createElementNS("http://www.w3.org/2000/svg","text");p.setAttribute("x",String(a)),p.setAttribute("y",String(s)),p.setAttribute("text-anchor","middle"),p.setAttribute("dominant-baseline","central"),p.textContent=String(d),n.appendChild(p);const u=()=>l.classList.add("-js-focussed"),h=()=>l.classList.remove("-js-focussed");e.addEventListener("focus",u),e.addEventListener("blur",h),o.focusHandlers.push({el:e,onFocus:u,onBlur:h}),r={x:a,y:s}}}function b(){if(c(),!o.options.headings&&!o.options.links&&!o.options.tabStops)return;const t=function(){const t=p(),e=t.querySelector("#"+n);return e.innerHTML='<p class="tsa-report-help">For each check, read and answer the questions shown.<br>The report is state dependant, ensure sections are expanded.</p>',t.classList.add("tsa-has-report"),e}();o.options.headings&&function(t){document.documentElement.setAttribute("data-tsa-headings-active","true");const e=document.createElement("div");let n='<h3>Headings</h3><ol><li>Is there a single main <code>H1</code> heading at the top of the page?</li><li>Looking at the list of headings, are they arranged in a logical order from highest (H1) to lowest (H2 to H6)?</li><li>Do all headings clearly describe the content that follows?</li><li>Have all empty headings been removed?</li></ol><ol class="tsa-indent">';const i=s(':is(h1,h2,h3,h4,h5,h6,[role="heading"])').filter(l);i.forEach(t=>{t.removeAttribute("data-tsa-heading-error"),t.setAttribute("data-tsa-heading-note","H"+u(t))});let a=6;const o=i.filter(t=>1===u(t));o.length||(n+='<li><span class="tsa-tag">H1</span> <span class="tsa-error">Missing H1</span></li>'),i.forEach((t,e)=>{const i=u(t),s="H"+i,[r,l]=function(t){let e=(t.textContent||"").trim();const n=t.querySelector("img"),i=!!n;return n&&(e?e+=` "${n.alt||""}"`:(n.getAttribute("alt")||"").trim()?e=`"${n.getAttribute("alt").trim()}"`:t.setAttribute("data-tsa-heading-error","true")),[e.trim(),i]}(t);let c="";r||(c=`Empty ${s} heading`),e>0&&i>a+1&&(c=`Should be a H${a+1}, or lower, but it's a ${s}`),1===i&&o.indexOf(t)>0&&(c="A single H1 heading is advised"),l&&!r&&(c=`${s} heading image is missing an alt text`),c&&(t.setAttribute("data-tsa-heading-note",c),t.setAttribute("data-tsa-heading-error","true")),n+=`<li class="${s}"><span class="tsa-tag">${s}</span> ${l?'<span class="tsa-img">IMG ALT:</span> ':""}<span>${d(r)}</span> ${c?`<strong class="tsa-error">${d(c)}</strong>`:""}</li>`,a=i}),n+="</ol>",e.innerHTML=n,t.appendChild(e)}(t),o.options.links&&m(t),o.options.tabStops&&f(t)}function x(){c()}p(),window[t]={run:b,clear:x,destroy:function(){x();const n=document.getElementById(e);n&&n.remove();const a=document.getElementById(i);a&&a.remove(),delete window[t]},ui:p},console.log("3 Step Self-Audit ready. Use the floating panel or window."+t+".run()")}();
Copy this snippet, then paste it into Chrome’s Console while viewing the webpage.
How to install
  1. Copy the minified console snippet.
  2. Open the web page you want to test.
  3. 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".
  4. Select the tab called "Console"".
    Developer Tools may open on a different tab first.
  5. Click in the Console input area, paste the code, then press Enter
    The testing panel and overlay should then appear on the page.
  6. Use the controls shown by the snippet to turn checks on or off.
Uncompressed snippet code (for the devs)
Full console snippet
(function () {
  'use strict';

  const NS = '__THREE_STEP_SELF_AUDIT__';
  const ROOT_ID = 'tsa-root';
  const PANEL_ID = 'tsa-panel';
  const REPORT_ID = 'tsa-report';
  const STYLE_ID = 'tsa-style';
  const SVG_ID = 'tsa-tabstops-svg';

  if (window[NS] && window[NS].destroy) {
    window[NS].destroy();
  }

  const state = {
    options: {
      headings: false,
      links: false,
      tabStops: false
    },
    cleanupFns: [],
    focusHandlers: [],
    observer: null
  };

  function qsa(sel, root = document) {
    return Array.from(root.querySelectorAll(sel));
  }

  function isInternal(el) {
    return !!(el && (el.closest('#' + ROOT_ID) || el.closest('#' + REPORT_ID) || el.closest('#' + SVG_ID)));
  }

  function isVisible(el) {
    if (!el || !el.isConnected || isInternal(el)) return false;
    if (typeof el.checkVisibility === 'function') {
      try {
        if (!el.checkVisibility()) return false;
      } 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;
  }

  function escapeHtml(str) {
    return String(str)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  function removeArtifacts() {
    document.documentElement.removeAttribute('data-tsa-headings-active');
    document.documentElement.removeAttribute('data-tsa-links-active');
    document.documentElement.removeAttribute('data-tsa-tabstops-active');
    qsa('[data-tsa-heading-note],[data-tsa-link-note],[data-tsa-tabstop]').forEach((el) => {
      el.removeAttribute('data-tsa-heading-note');
      el.removeAttribute('data-tsa-link-note');
      el.removeAttribute('data-tsa-tabstop');
      el.classList.remove('tsa-tabable');
    });
    state.focusHandlers.forEach(({ el, onFocus, onBlur }) => {
      el.removeEventListener('focus', onFocus);
      el.removeEventListener('blur', onBlur);
    });
    state.focusHandlers = [];
    state.cleanupFns.forEach((fn) => {
      try { fn(); } catch (_) {}
    });
    state.cleanupFns = [];
    const svg = document.getElementById(SVG_ID);
    if (svg) svg.remove();
    const root = document.getElementById(ROOT_ID);
    const report = root ? root.querySelector('#' + REPORT_ID) : null;
    if (report) report.innerHTML = '';
    if (root) root.classList.remove('tsa-has-report');
  }

  function ensureStyle() {
    const existing = document.getElementById(STYLE_ID);
    if (existing) return existing;
    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = `
#${ROOT_ID}{position:fixed;top:16px;left:16px;z-index:2147483646;background:#f7f7f7;border:2px solid #ccc;padding:12px 14px;font:400 14px/1.4 Arial,sans-serif;color:#222;width:320px;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);display:flex;flex-direction:column;gap:0;box-shadow:0 4px 14px rgba(0,0,0,.18);box-sizing:border-box}
#${ROOT_ID} h2{margin:0 0 8px;font:700 16px/1.2 Arial,sans-serif;cursor:move}
#${ROOT_ID} fieldset{border:0;padding:0;margin:8px 0 10px}
#${ROOT_ID} label{display:flex;gap:8px;align-items:center;margin:6px 0}
#${ROOT_ID} .tsa-controls{flex:0 0 auto}
#${ROOT_ID} .tsa-help{margin:0 0 8px;color:#444;font-size:12px}
#${REPORT_ID}{display:none;flex:1 1 auto;min-height:0;overflow:auto;background:#f7f7f7;border:0;border-top:1px solid #ccc;padding:4px 0 0;font:400 13px/1.4 Arial,sans-serif;color:#333;box-shadow:none;box-sizing:border-box;margin-top:8px}
#${ROOT_ID}.tsa-has-report #${REPORT_ID}{display:block}
#${REPORT_ID} h2{margin:0 0 8px;font:700 20px/1.2 Arial,sans-serif}
#${REPORT_ID} h3{margin:28px 0 8px;font:700 17px/1.2 Arial,sans-serif}
#${REPORT_ID} .tsa-report-help{margin:10px 0 10px;color:#333;font-size:12px}
#${REPORT_ID} > :first-child h3,#${REPORT_ID} > h3:first-child{margin-top:0}
#${REPORT_ID} ol, #${REPORT_ID} ul{margin:.7rem 0;padding-left:1rem}
#${REPORT_ID} .tsa-indent{list-style:none;padding-left:0}
#${REPORT_ID} .tsa-indent li{margin-top:6px}
#${REPORT_ID} .tsa-tag{display:inline-block;background:green;color:#fff;padding:2px 4px 0;text-transform:uppercase;margin-right:6px}
#${REPORT_ID} .tsa-error{color:#c00;font-weight:700}
#${REPORT_ID} .tsa-img{color:#070}
#${REPORT_ID} a,#${REPORT_ID} a:visited{color:#1b5e20!important;text-decoration:underline!important;text-underline-offset:2px;font-weight:500}
#${REPORT_ID} .H1{padding-left:0} #${REPORT_ID} .H2{padding-left:24px} #${REPORT_ID} .H3{padding-left:48px} #${REPORT_ID} .H4{padding-left:72px} #${REPORT_ID} .H5{padding-left:96px} #${REPORT_ID} .H6{padding-left:120px}
html[data-tsa-headings-active="true"] :is(h1,h2,h3,h4,h5,h6,[role="heading"]):not(#${ROOT_ID} *, #${REPORT_ID} *){--tsa-h-color:#000;--tsa-h-bg:#0c0;outline:2px solid var(--tsa-h-bg)!important;outline-offset:-2px;position:relative}
html[data-tsa-headings-active="true"] :is(h1,h2,h3,h4,h5,h6,[role="heading"]):not(#${ROOT_ID} *, #${REPORT_ID} *)::after{content:attr(data-tsa-heading-note);color:var(--tsa-h-color)!important;background:var(--tsa-h-bg)!important;text-shadow:0 0 1px #000;box-sizing:border-box;z-index:2147483640;display:flex;align-items:center;position:absolute;inset:0 0 auto auto;padding:2px 6px;font:500 14px/1.3 Arial,sans-serif;max-width:55%;min-height:100%}
html[data-tsa-headings-active="true"] [data-tsa-heading-note][data-tsa-heading-error="true"]{--tsa-h-color:#fff;--tsa-h-bg:#e50000;outline:4px dotted var(--tsa-h-bg)!important;min-height:1.5rem}
html[data-tsa-links-active="true"] a[href]:not(#${ROOT_ID} *, #${REPORT_ID} *){outline:4px solid green!important;outline-offset:2px!important}
html[data-tsa-links-active="true"] a[href][data-tsa-link-note]:not(#${ROOT_ID} *, #${REPORT_ID} *){outline:4px dotted #c00!important;position:relative;display:inline-block}
html[data-tsa-links-active="true"] a[href][data-tsa-link-note]:not(#${ROOT_ID} *, #${REPORT_ID} *)::after{content:attr(data-tsa-link-note);position:absolute;left:100%;top:0;margin-left:8px;background:#c00;color:#fff;padding:2px 6px;font:500 14px/1.3 Arial,sans-serif;text-shadow:0 0 1px #000;z-index:2147483640;box-sizing:border-box;display:flex;align-items:center;min-height:100%;width:max-content}
html[data-tsa-links-active="true"] a[href]:focus,html[data-tsa-links-active="true"] a[href]:focus-visible{outline-style:dashed!important}
#${SVG_ID}{position:absolute;inset:0;overflow:visible;pointer-events:none;z-index:2147483644;margin:0}
#${SVG_ID} line{stroke:yellow;stroke-width:4px}
#${SVG_ID} rect{fill:yellow;stroke:black;stroke-width:1px}
#${SVG_ID} text{fill:black;font-size:10px;font-weight:700}
#${SVG_ID} rect.-js-focussed{fill:red!important}
`;
    document.head.appendChild(style);
    return style;
  }

  function ensureUI() {
    let root = document.getElementById(ROOT_ID);
    if (root) return root;
    ensureStyle();
    root = document.createElement('div');
    root.id = ROOT_ID;
    root.innerHTML = `
      <div class="tsa-controls">
        <h2>Accessibility self-audit</h2>
        <p class="tsa-help">Tick a check to turn it on. Untick all checks to clear the overlay.</p>
        <fieldset>
          <label><input type="checkbox" data-opt="headings"> Headings</label>
          <label><input type="checkbox" data-opt="links"> Links</label>
          <label><input type="checkbox" data-opt="tabStops"> Tab-stops</label>
        </fieldset>
      </div>
      <output id="${REPORT_ID}" class="reportWindow"></output>
    `;
    root.addEventListener('change', (e) => {
      const input = e.target;
      if (input && input.matches('[data-opt]')) {
        state.options[input.getAttribute('data-opt')] = input.checked;
        run();
      }
    });
    document.body.appendChild(root);
    const handle = root.querySelector('h2');
    let drag = null;
    const onMove = (event) => {
      if (!drag) return;
      const maxLeft = Math.max(0, window.innerWidth - root.offsetWidth);
      const maxTop = Math.max(0, window.innerHeight - root.offsetHeight);
      const left = Math.min(maxLeft, Math.max(0, event.clientX - drag.dx));
      const top = Math.min(maxTop, Math.max(0, event.clientY - drag.dy));
      root.style.left = left + 'px';
      root.style.top = top + 'px';
      root.style.right = 'auto';
    };
    const onUp = () => {
      drag = null;
      document.removeEventListener('pointermove', onMove);
      document.removeEventListener('pointerup', onUp);
    };
    handle.addEventListener('pointerdown', (event) => {
      if (event.target.closest('button,input,label')) return;
      const rect = root.getBoundingClientRect();
      drag = { dx: event.clientX - rect.left, dy: event.clientY - rect.top };
      document.addEventListener('pointermove', onMove);
      document.addEventListener('pointerup', onUp);
    });
    return root;
  }

  function createReport() {
    const root = ensureUI();
    const report = root.querySelector('#' + REPORT_ID);
    report.innerHTML = '<p class="tsa-report-help">For each check, read and answer the questions shown.<br>The report is state dependant, ensure sections are expanded.</p>';
    root.classList.add('tsa-has-report');
    return report;
  }

  function headingText(el) {
    let text = (el.textContent || '').trim();
    const img = el.querySelector('img');
    const hasImage = !!img;
    if (img) {
      if (text) text += ` "${img.alt || ''}"`;
      else if ((img.getAttribute('alt') || '').trim()) text = `"${img.getAttribute('alt').trim()}"`;
      else el.setAttribute('data-tsa-heading-error', 'true');
    }
    return [text.trim(), hasImage];
  }

  function headingLevel(el) {
    const aria = el.getAttribute('aria-level');
    if (aria) return parseInt(aria, 10) || 6;
    const m = el.tagName.toUpperCase().match(/^H([1-6])$/);
    return m ? parseInt(m[1], 10) : 6;
  }

  function runHeadings(report) {
    document.documentElement.setAttribute('data-tsa-headings-active', 'true');
    const section = document.createElement('div');
    let html = '<h3>Headings</h3><ol><li>Is there a single main <code>H1</code> heading at the top of the page?</li><li>Looking at the list of headings, are they arranged in a logical order from highest (H1) to lowest (H2 to H6)?</li><li>Do all headings clearly describe the content that follows?</li><li>Have all empty headings been removed?</li></ol><ol class="tsa-indent">';
    const headings = qsa(':is(h1,h2,h3,h4,h5,h6,[role="heading"])').filter(isVisible);
    headings.forEach((el) => {
      el.removeAttribute('data-tsa-heading-error');
      el.setAttribute('data-tsa-heading-note', 'H' + headingLevel(el));
    });
    let lastLevel = 6;
    const h1s = headings.filter((el) => headingLevel(el) === 1);
    if (!h1s.length) {
      html += `<li><span class="tsa-tag">H1</span> <span class="tsa-error">Missing H1</span></li>`;
    }
    headings.forEach((el, idx) => {
      const level = headingLevel(el);
      const tag = 'H' + level;
      const [text, hasImage] = headingText(el);
      let issue = '';
      if (!text) issue = `Empty ${tag} heading`;
      if (idx > 0 && level > lastLevel + 1) issue = `Should be a H${lastLevel + 1}, or lower, but it's a ${tag}`;
      if (level === 1 && h1s.indexOf(el) > 0) issue = 'A single H1 heading is advised';
      if (hasImage && !text) issue = `${tag} heading image is missing an alt text`;
      if (issue) {
        el.setAttribute('data-tsa-heading-note', issue);
        el.setAttribute('data-tsa-heading-error', 'true');
      }
      html += `<li class="${tag}"><span class="tsa-tag">${tag}</span> ${hasImage ? '<span class="tsa-img">IMG ALT:</span> ' : ''}<span>${escapeHtml(text)}</span> ${issue ? `<strong class="tsa-error">${escapeHtml(issue)}</strong>` : ''}</li>`;
      lastLevel = level;
    });
    html += '</ol>';
    section.innerHTML = html;
    report.appendChild(section);
  }

  function linkText(link) {
    let text = (link.textContent || '').trim();
    const img = link.querySelector('img');
    if (img) text += ` "${img.getAttribute('alt') || ''}"`;
    return [text.trim(), !!img];
  }

  function duplicateTextDifferentHref(links, current, txt, href) {
    return links.some((other) => other !== current && isVisible(other) && linkText(other)[0] === txt && other.getAttribute('href') !== href);
  }

  function genericLink(link) {
    return ['see all','see more','view all','view more','read more','more','click here','here','find out more'].includes((link.textContent || '').trim().toLowerCase());
  }

  function opensNewWindowWithoutIndicator(link) {
    return (link.getAttribute('target') || '').toLowerCase() === '_blank' && getComputedStyle(link, '::after').content === 'none';
  }

  function documentLink(link) {
    if (link.href.startsWith('javascript:')) return false;
    return ['.pdf','.xlsx','.docx','.pptx'].some((ext) => link.href.includes(ext));
  }

  function runLinks(report) {
    document.documentElement.setAttribute('data-tsa-links-active', 'true');
    const section = document.createElement('div');
    let html = '<h3>Links</h3><ol style="margin:1rem 0"><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 "More details"?</li><li>Does the link text accurately describe the destination it goes to?</li><li>Do links with the same text go to the same destination?</li><li>Do links use additional methods, apart from colour, to be distinguishable?</li><li>Are links that open in a new window or tab clearly indicated?</li><li>Are document links clearly marked with format and file size?</li></ol><ol class="tsa-indent">';
    const links = qsa('a[href]').filter(isVisible);
    links.forEach((link) => link.removeAttribute('data-tsa-link-note'));
    links.forEach((link) => {
      let issue = '';
      const [txt, hasImg] = linkText(link);
      let href = link.getAttribute('href') || '';
      if (href && href.startsWith('javascript:')) href = 'javascript:';
      if (href) {
        if (duplicateTextDifferentHref(links, link, txt, href)) issue += 'Duplicated link copy, with a different address. ';
        const img = link.querySelector('img');
        if (img) {
          const alt = img.getAttribute('alt') || '';
          if (!((link.textContent || '') + alt).trim().length) issue += 'Link image missing alt text. ';
        }
        if (genericLink(link)) issue += 'Does this link text describe the destination? ';
        if (opensNewWindowWithoutIndicator(link)) issue += 'Is this link marked as opening in a new window or tab? ';
        if (documentLink(link)) {
          const u = new URL(link.href);
          u.search = '';
          u.hash = '';
          const str = u.toString();
          const ext = str.indexOf('.') > 0 ? str.split('.').pop().toLowerCase() : 'undefined';
          if (!(link.textContent || '').toLowerCase().includes(ext)) issue += 'Is this document link marked with format and file-size? ';
        }
      } else if (href === '') {
        issue += 'Link missing address. ';
      }
      issue = issue.trim();
      if (issue) link.setAttribute('data-tsa-link-note', issue);
      html += `<li>${hasImg ? '<span class="tsa-img">alt-text: </span>' : ''}<a ${href ? `href="${escapeHtml(href)}"` : ''} class="copy">${escapeHtml(txt)}</a> ${issue ? `<strong class="tsa-error">${escapeHtml(issue)}</strong>` : ''}</li>`;
    });
    html += '</ol><p>Links in the content are highlighted: valid links are marked with a 4px solid green outline; invalid links are marked with a 4px dotted red outline; the outline becomes dashed on focus.</p>';
    section.innerHTML = html;
    report.appendChild(section);
  }

  function radioRepresentative(name) {
    const radios = qsa(`input[type="radio"][name="${CSS.escape(name)}"]`).filter(isVisible).filter((el) => !isInternal(el));
    if (!radios.length) return null;
    return radios.find((el) => el.checked) || radios[0];
  }

  function tabbables() {
    return qsa(':is(a[href],area[href],audio[controls],button,embed[src],iframe,input,select,summary,textarea,video,[contenteditable],[tabindex]):not([hidden],:disabled,[tabindex^="-"])').filter(isVisible);
  }

  function runTabStops(report) {
    document.documentElement.setAttribute('data-tsa-tabstops-active', 'true');
    const section = document.createElement('div');
    section.innerHTML = '<h3>Keyboard tab-stops</h3><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 <b>expand accordions</b> before testing.</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—specifically 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>';
    report.appendChild(section);

    let svg = document.getElementById(SVG_ID);
    if (svg) svg.remove();
    svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.id = SVG_ID;
    document.body.appendChild(svg);
    const items = tabbables();
    let prev = null;
    let num = 0;
    let seenRadio = '';
    for (const item of items) {
      if (item.name && item.name === seenRadio) continue;
      let focusEl = item;
      if (item.type === 'radio') {
        focusEl = radioRepresentative(item.name);
        seenRadio = item.name;
        if (!focusEl) continue;
      }
      focusEl.classList.add('tsa-tabable');
      focusEl.setAttribute('data-tsa-tabstop', 'true');
      const rect = focusEl.getBoundingClientRect();
      const x = Math.round(rect.left + rect.width / 2);
      const y = Math.round(window.scrollY + rect.top + rect.height / 2);
      if (prev) {
        const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        line.setAttribute('x1', String(prev.x));
        line.setAttribute('y1', String(prev.y));
        line.setAttribute('x2', String(x));
        line.setAttribute('y2', String(y));
        svg.appendChild(line);
      }
      num += 1;
      const r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
      r.setAttribute('x', String(x - 11));
      r.setAttribute('y', String(y - 11));
      r.setAttribute('width', '22');
      r.setAttribute('height', '22');
      svg.appendChild(r);
      const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
      t.setAttribute('x', String(x));
      t.setAttribute('y', String(y));
      t.setAttribute('text-anchor', 'middle');
      t.setAttribute('dominant-baseline', 'central');
      t.textContent = String(num);
      svg.appendChild(t);
      const onFocus = () => r.classList.add('-js-focussed');
      const onBlur = () => r.classList.remove('-js-focussed');
      focusEl.addEventListener('focus', onFocus);
      focusEl.addEventListener('blur', onBlur);
      state.focusHandlers.push({ el: focusEl, onFocus, onBlur });
      prev = { x, y };
    }
  }

  function run() {
    removeArtifacts();
    if (!state.options.headings && !state.options.links && !state.options.tabStops) {
      return;
    }
    const report = createReport();
    if (state.options.headings) runHeadings(report);
    if (state.options.links) runLinks(report);
    if (state.options.tabStops) runTabStops(report);
  }

  function clear() {
    removeArtifacts();
  }

  function destroy() {
    clear();
    const root = document.getElementById(ROOT_ID);
    if (root) root.remove();
    const style = document.getElementById(STYLE_ID);
    if (style) style.remove();
    delete window[NS];
  }

  ensureUI();
  window[NS] = { run, clear, destroy, ui: ensureUI };
  console.log('3 Step Self-Audit ready. Use the floating panel or window.' + NS + '.run()');
})();
          
1. Headings

Heading checks

What you need to check

1. Is there a single main H1 heading at the top of the page? Is there a main heading at the top of the page?
2. Are headings arranged in a logical order from highest (H1) to lowest (H2 to H6)? Are headings arranged in a logical order from highest to lowest?
3. Do all headings clearly introduce, or describe, the content that follows?
4. Have all empty headings been removed?

References & resources

To help meet the guidelines

2. Links
3. Keyboard

Keyboard checks

A bluetooth keyboard is required to test on iOS and Android.

Keyboard navigation on iOS or Android

How to test Keyboard navigation on iOS or Android

Native Apps: Keyboard Navigation Testing

Use only the keyboard to navigate through the document by pressing the tab and shift + tab keys, or the arrow keys when selecting radio buttons. Activate links by pressing the Return key and use either the Return or Spacebar to activate buttons.

What you need to check

1. When an element is focused by keyboard, is your current position always clearly indicated?
2. Are all interactive elements, such as buttons, links, and input fields, reachable and usable with just the keyboard?
3. Does keyboard focus stop on any non-interactive elements?
4. Are there any elements which remain invisible upon keyboard focus?
5. Are all links, buttons, and inputs, adequately sized and spaced? When running tab-stops check from the console script, are all links, buttons, and inputs, larger than the overlaid yellow square? Are all links, buttons, and inputs, adequately sized and spaced? Are any too small, or too close to a neighbour, to easily tap?
6. Does the keyboard focus order match the visual layout? Specifically from left to right and then top to bottom.
7. Are there any points where the keyboard becomes stuck? E.g. does pressing Esc or Tab not allow you to exit a component?

References & resources

To help meet the guidelines

4. Text Resize

Font scaling or text resizing

Set up Chrome browser to test text-resizing and reflow

How to set up Chrome for testing text-resizing and reflow

  1. Open the Chrome browser and maximise it to fill the screen (this guide assumes your display is 1280 pixels or wider).
  2. Access Chrome DevTools by clicking:
    - On PC: Press F12 or Control + Shift + I
    - On Mac: Press Command + Option + I
  3. The DevTools panel should open on the right-hand side. If it doesn’t, look for the three vertical dots icon in the top right corner of the DevTools panel, click it, and set the "Dock side" to the right.
  4. Adjust the border between the main window and the DevTools panel by dragging it until the main window width reaches 1280 pixels.
  5. To increase the font size, press:
    - PC: CTRL + +
    - Mac: Command + +
    Repeat this step eight times until the text zoom level reaches 400%.

We're now ready to begin testing the page…

Set up Firefox browser to test text-resizing and reflow

How to set up Firefox for testing text-resizing and reflow

  1. Open Firefox browser and maximise it to full screen but keep the Menu bar in view.
  2. Navigate to the page you wish to test.
  3. From the Menu bar, click View, Zoom, and ensure Zoom Text Only is ticked
  4. Again from the Menu bar, click Tools, then Browser Tools, then Responsive Design Mode.
  5. In the Responsive Design Mode window controls, set width to 1280px, height to 800 (or almost full browser height), and set DPR: 1.
  6. To increase the font size, press:
    - PC: CTRL + +
    - Mac: Command + +
    Repeat this step none times until the text zoom level reaches 400%.

We're now ready to begin testing the page…

Text-resizing on iOS or Android

How to test WCAG 1.4.4 text-resize on iOS or Android

Native Apps: Dynamic Type and Font Size Testing

What you need to check

Scale the text content up to 400% on a browser or device with a width of 1280px, then verfy the following:

Scale the text content up to 200% on a browser or device with a width of 320px, then verfy the following:

1. Did all the text scale-up as expected?
2. Is there any need to scroll horizontally, with an exception for data tables?
3. Has any text been obscured, hidden, or cut off?
4. Have long words been broken or hyphenated over two lines as necessary?
5. Have words trunkated so much that they're meaning has been lost?

Resources & references

To help meet the guidelines

5. Automated testing

Automated testing

Automated testing is an excellent method for identifying common errors.

We recommend that product managers and owners use Lighthouse, which is integrated into both Chrome and Edge browsers. For developers, we suggest installing the Deque Axe DevTool extension for automated testing.

Other automated testing tools

External testing online (must be public facing):

Browser extensions (requires installation):

Server-side testing:

Silktide server– Is available on the back-end of Santander servers.

App testing

BrowserStack and Sauce Labs are industry-leading, cloud-based web and mobile testing platforms for developers. Both platforms offer quality features to streamline and simplify testing across browsers and devices.

Currently, we recommend the use of Sauce Labs as it has built in Deque Axe DevTools for automated accessibility testing.

Automated testing is available from:

Lists of software to test Accessibility on mobile apps:

Lighthouse - How to run an automated test

To perform an accessibility test using Lighthouse, first open your website in Chrome. Then, access DevTools and go to the Lighthouse tab. Select "Accessibility" and choose the device type, either desktop or mobile. Finally, click "Analyze page load" to start the test.

Video: Google Lighthouse | Automated Accessibility Tool Review

Detailed Steps:
  1. Open the webpage in Chrome: Navigate to the webpage you want to audit in Google Chrome.
  2. Open DevTools: Right-click anywhere on the page and select "Inspect" to open the DevTools.
  3. Access Lighthouse: In DevTools, on the left side of the window, click on the "Lighthouse" tab.
  4. Choose Accessibility: Under the "Categories" section in Lighthouse, select "Accessibility".
  5. Select Device Type: Choose either "Desktop" or "Mobile" to simulate the device type you want to test.
  6. Generate Report: Click the "Generate report" button to initiate the audit.
  7. Review the Report: Lighthouse will display a score and a detailed report outlining passed and failed accessibility audits.
  8. Analyse Results: The report will categorize failed audits (e.g., contrast, navigation) and provide information about the issues, including descriptions, links to more information, and code snippets.

Introduction to Lighthouse

What you need to check

From the automated test results answer the following:

1. Do all form elements have associated labels?
2. Do all image elements have appropriate alt attributes?
3. Do background and foreground colors have a sufficient colour contrast?
4. Are ARIA attributes, when applied, used as intended?
5. Did any other errors arise?

Ask the developer(s) to repair any issues raised.

Assessment results

Assessment summary by section
SectionQuestionsPassedFailedNot ApplicablePass/Fail
Headings4000
Links7000
Keyboard navigation7000
Text resize4000
Automated testing5000
Total questions27Unanswered27

How to save assessment results

Saving this assessment

To save this page as a PDF document containing just the relevant form data

  1. Click the "Save assessment as PDF document" button.
  2. In the Print window under "Destination" select "Save as PDF".
  3. Click "Save".
  4. By default the file will be named "Accessibility Assessment (Assessment-Date).pdf" change the name if you wish.
  5. It will save to the browsers default location. Change if required.

Note: Using the browsers default print option will output the whole page including the instructions.

Once the assessment is completed, please email it to tom.paget@santander.co.uk

Older bookmarklets

Bookmarklets

Kept for archive and have been superseded by the console script outlined above. This section is kept for reference only.

The bookmarklets only work within a browser on web pages. We recommend Firefox as a browser for this purpose. It should be available from the self-service app but you can also get it here: Download Firefox.

Bookmarklet installation instructions

  1. Drag each of the three bookmarklets into your browser's bookmarks bar:
  2. Navigate to the page you wish to test.
  3. Click the bookmark.
  4. Answer the check questions asked.

Bookmarklets source code

Dated 03/09/2025

This webpage is also available as a Word document: Accessibility Self-Assessment [docx 31Kb]