<div id=top></div>

<a class=skip_lnk-content href=#main_h1>Skip to content</a>

<nav class=anchorNav id=top>
  <ul class=anchorNav_lst>
    <li class=anchorNav_li><a class=anchorNav_lnk href=#code>Code</a></li>
    <li class=anchorNav_li><a class=anchorNav_lnk href=#credits>Credits</a></li>
  </ul>
</nav>

<main class=main>

  <!-- tabindex = -1 is required for iOS with JS unavailable -->
  <h1 class=main_h1 id=main_h1>Accessible in-page anchors for desktop and mobile</h1>

  <p class=main_p>Browsers and assistive technologies have issues when using in-page anchors (skip links)</p>

  <p class=main_p>Android and iOS have issue if <code class=language-html>tabindex="-1"</code> is missing off the target element. Android requires it programatically added and focus shifted.</p>
  
  <p class=main_p>While using <code class=language-html>tabindex</code> on content blocks, which contain focusable elements (link, input, video, etc), may, when escaping that elements focus, move the focus caret to the content block itself. Which is disorientating and far from ideal.</p>
<!--
  <p class=main_p>For a full description of the issue please see this <a class=fancyLnk href="https://github.com/alphagov/govuk_elements/pull/534">Gov UK bug report</a>.</p>
-->  
  <p class=main_p>The solution presented includes skip-to-content links and  corrects the experienced side-issues across AT, desktop, iOS and Android by adding, and removing, <code class=language-html>tabindex="-1"</code> on-demand using vanilla JavaScript.</p>
  
  <p class=main_p><strong>Please note</strong>: <code class=language-html>tabindex="-1"</code> focused elements are outlined in white and coloured red for this demonstration. Purely to highlight the difference between focusing on a heading and a block.</p>
  
  <section id=code>

    <h2 class=main_h2>Oh, that beautiful code!</h2>
  
    <figure>
      <figcaption>HTML</figcaption>
      <pre><code class=language-markup spellcheck=false contenteditable>&lt;div id="top"&gt;&lt;/div&gt;
&lt;a class="skip_lnk-content" href="#main"&gt;Main content&lt;/a&gt;

&lt;!-- Nav, etc --&gt;

&lt;!-- In-page anchors --&gt;
&lt;a href="#code"&gt;Code&lt;/a&gt;
&lt;a href="#credits"&gt;Credits&lt;/a&gt;

&lt;main id="main"&gt;

  &lt;section id="code"&gt;
    &lt;!-- May contain focusable elements --&gt;
  &lt;/section&gt;

  &lt;section&gt;
    &lt;!-- Personally, I prefer to link to the heading --&gt;
    &lt;h2 id="credits"&gt;Credits&lt;/h2&gt;
    &lt;!-- May contain focusable elements --&gt;
  &lt;/section&gt;

  &lt;a href="#top"&gt;Back to top&lt;/a&gt;

&lt;/main&gt;
</code></pre>
    </figure>

    <figure>
      <figcaption>CSS - scroll behaviour</figcaption>
      <pre><code class=language-css spellcheck=false contenteditable>html {
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}
</code></pre>
    </figure>

    <p class=main_p>Nice smooth scrolling, unless the user has declared an alternate preference.</p>

    <figure>
      <figcaption>CSS - skip-to-content</figcaption>
      <pre><code class=language-css spellcheck=false contenteditable>.skip_lnk-content {
  position: absolute;
  padding: .5rem 1rem;
  color: #fff;
  background-color: #3B2D4A;
  text-decoration: none;
  font-weight: bold;
  z-index: 10;
  transform: translate3d(.125rem, -5rem, 0);
  transition: transform .3s ease-out;
}
.skip_lnk-content:focus {
  transform: translate3d(.125rem, .125rem, 0);
  outline: #fff solid .125rem;
}
@media print {
  .skip_lnk-content {
    display: none;
  }
}
</code></pre>
    </figure>

    <p class=main_p>The skip-to-content link only appears when accessing the page via keyboard.</p>

    <figure>
      <figcaption>JavaScript (ES6)</figcaption>
      <pre><code class=language-javascript spellcheck=false contenteditable>// Add tabindex, to target, on clicking an in-page anchor
//   - Remove tabindex on bluring the target element.
const inPageAnchors = (_ =&gt; {
  // return;
  'use strict'
  const currentURL = self.location.href;

  const scrollAndFocus = target =&gt; {
    target.setAttribute('tabindex', '-1');
    target.addEventListener('blur', _ =&gt; target.removeAttribute('tabindex'), {once: true});
    requestAnimationFrame(_ =&gt; {
      target.scrollIntoView();
      target.focus({preventScroll:true});
    });
  };

  const anchorClicked = e =&gt; {
    const a = e.target;
    const to_id = a.hash.split('#')[1];
    const to_obj = document.getElementById(to_id);

    // If invalid return retaining default behaviour.
    if (!to_obj) return;

    e.preventDefault();
    
    // Update the URL fragment identifier
    // - so the back button works!
    window.location.hash = to_id;

    scrollAndFocus(to_obj);
  };

  const anchors = document.getElementsByTagName('a');
  for (const a of anchors) {

    // Filter for in-page anchors only!
    if (!a.hash) continue;
    if (a.origin + a.pathname !== self.location.origin + self.location.pathname) continue;

    a.addEventListener('click', anchorClicked);
  }

})();
</code></pre>
    </figure>

    <figure>
      <figcaption>JavaScript ES6 <small>(UglifyJS 3 - 532 bytes)</small></figcaption>
      <pre><code class=language-javascript spellcheck=false contenteditable>const inPageAnchors=(_=&gt;{"use strict";self.location.href;const t=e=&gt;{const t=e.target.hash.split("#")[1],n=document.getElementById(t);n&amp;&amp;(e.preventDefault(),window.location.hash=t,(e=&gt;{e.setAttribute("tabindex","-1"),e.addEventListener("blur",t=&gt;e.removeAttribute("tabindex"),{once:!0}),requestAnimationFrame(t=&gt;{e.scrollIntoView(),e.focus({preventScroll:!0})})})(n))},n=document.getElementsByTagName("a");for(const e of n)e.hash&amp;&amp;e.origin+e.pathname===self.location.origin+self.location.pathname&amp;&amp;e.addEventListener("click",t)})();
</code></pre>
    </figure>

    <figure>
      <figcaption>JavaScript ES5 <small>(Google Closure - 989 bytes)</small></figcaption>
      <pre><code class=language-javascript spellcheck=false contenteditable>var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(a){var c=0;return function(){return c&lt;a.length?{done:!1,value:a[c++]}:{done:!0}}};$jscomp.arrayIterator=function(a){return{next:$jscomp.arrayIteratorImpl(a)}};$jscomp.makeIterator=function(a){var c="undefined"!=typeof Symbol&amp;&amp;Symbol.iterator&amp;&amp;a[Symbol.iterator];return c?c.call(a):$jscomp.arrayIterator(a)};var inPageAnchors=function(a){var c=function(a){a.setAttribute("tabindex","-1");a.addEventListener("blur",function(b){return a.removeAttribute("tabindex")},{once:!0});requestAnimationFrame(function(b){a.scrollIntoView();a.focus({preventScroll:!0})})};a=function(a){var b=a.target.hash.split("#")[1],d=document.getElementById(b);d&amp;&amp;(a.preventDefault(),window.location.hash=b,c(d))};var d=document.getElementsByTagName("a");d=$jscomp.makeIterator(d);for(var b=d.next();!b.done;b=d.next())b=b.value,b.hash&amp;&amp;b.origin+b.pathname===self.location.origin+self.location.pathname&amp;&amp;b.addEventListener("click",a)}();
</code></pre>
    </figure>

  </section>


  <section>
  
    <h2 class=main_h2 id=credits>Credits and references</h2>

    <p class=main_p>For a full description of the issue faced on mobile devices, and who it affects, please see the original article by Hampus Sethfors: <a class=fancyLnk rel=noreferrer target=_blank href="https://axesslab.com/skip-links/">Your skip links are broken</a>.</p>

    <p class=main_p>The UK Gov website raised this issue with applying focus on <code class=language-html>main</code>: <a class=fancyLnk href="https://github.com/alphagov/govuk_elements/pull/534">bug report</a></p>

    <p class=main_p>A jQuery skip-link fix is available here: <a class=fancyLnk rel=noreferrer target=_blank href="https://github.com/selfthinker/dokuwiki_template_writr/blob/master/js/skip-link-focus-fix.js">Skip link focus fix</a></p>

    <p class=main_p>Just a vanilla skip-to-content fix is available here: <a class=fancyLnk rel=noreferrer target=_blank href="https://codepen.io/2kool2/pen/bxdzEJ">Accessible skip-to-content</a>.</p>

    <h3 class=main_h3>Coding</h3>

    <p class=main_p>Responsive font sizing: <a class=fancyLnk rel=noreferrer target=_blank href="https://websemantics.uk/tools/responsive-font-calculator/" href=#main_h1>Fluid-responsive font-size calculator</a></p>

    <p class=main_p>Codepen: <a class=fancyLnk rel=noreferrer target=_blank href="https://codepen.io/2kool2/pen/yLNjaVZ" href=#main_h1>Fancier text links</a></p>

    <p class=main_p>Codepen: <a class=fancyLnk rel=noreferrer target=_blank href="https://codepen.io/2kool2/pen/MEbeEg" href=#main_h1>Prism code highlighting</a></p>

  </section>

  <p class=pageAnchor-right><a class=fancyLnk href=#main_h1>Back to content top (h1)</a></p>

  <p class=pageAnchor-right><a class=fancyLnk href=#top>Back to page top</a></p>
  
</main>



<!-- Footer codepen include -->
[[[https://codepen.io/2kool2/pen/mKeeGM]]]
html {
  box-sizing: border-box;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}
*,
*::before,
*::after {box-sizing: inherit;}
body {

  /* Fix Safari bug with viewport units in calc() */
  min-height: 0vw;

  /* Fluidly variable between: 16px @ 320px and 32px @ 1920px */
  --space: calc(1rem + ((1vw - 0.2em) * 1));

  margin: 0;
  color: #666;
  font-size: var(--space);
  font-family: sans-serif;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: hsl(269,19%,30%);
  background-color: hsla(32,100%,85%,.35);
  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg width='100%25' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.4'/%3E%3C/filter%3E%3C/defs%3E%3C!-- %3Cpath filter='url(%23a)' opacity='.3' d='M0 0h1200v256H0z'/%3E--%3E%3Crect filter='url(%23a)' opacity='.3' width='100%25' height='100%25'/%3E%3C/svg%3E");
}
::-moz-selection {
  color: #fff;
  background-color: #000;
}
::selection {
  color: #fff;
  background-color: #000;
}


/* Skip link */

.skip_lnk-content {
  position: absolute;
  padding: .5rem 1rem;
  color: #fff;
  background-color: #3B2D4A;
  text-decoration: none;
  font-weight: bold;
  z-index: 10;
  transform: translate3d(.125rem, -5rem, 0);
  transition: transform .3s ease-out;
}
.skip_lnk-content:focus {
  transform: translate3d(.25rem, .25rem, 0);
  outline: #fff solid .125rem;
}
@media print {
  .skip_lnk-content {
    display: none;
  }
}


/* In-page anchor nav */
.anchorNav {
  color: #fff;
  background-color: #463b4c;
  border: 2px solid #463b4c;
}
.anchorNav_lst {
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: space-around;
  list-style: none;
}
.anchorNav_li {}
.anchorNav_lnk {
  display: block;
  font-weight: bold;
  color: #fff;
  padding: .5em;
}
.anchorNav_lnk:focus {
  outline: #fff solid .125rem;
}
@media print {
  .anchorNav {
    display: none;
  }
}


/* Main */
.main {padding: 0 1rem;}
.main_h1 {
  font-weight: 100;
  line-height: 1.3;
  text-align: center;
  margin-top: 2rem;
}
.main_h2 {
  font-size: 2rem;
  font-weight: 100;
  line-height: 1.3;
  text-align: center;
  margin-top: 3rem;
}
.main_h3 {
  font-size: 1.75rem;
  font-weight: 100;
  line-height: 1.3;
  text-align: center;
  margin-top: 3rem;
}


/* 
  Not required.
  It just allows you to see where the focus lands.
  Notice the difference between focusing on a block and a heading though.
*/
[tabindex="-1"]:focus {
  color: red;
  outline: #fff solid .25rem;
  outline: none;
  box-shadow:
     inset .25rem .25rem 0rem #fff,
     inset -.25rem -.25rem 0rem #fff;
}


.main_h1 + .main_p {
  font-size: larger;
  line-height: 1.3;
  text-align: center;
}
.main_p {
  text-align: left;
  max-width: 65ch;
  margin: 1rem auto;
}
.main_lnk,
.main_lnk:visited {
  white-space: nowrap;
  color: #236ECE;
}
.main_lnk:hover,
.main_lnk:focus {
  color: #014cac;
}
.main_code {font-size:1rem}



/* Back to top anchors */
.pageAnchor-right {
  text-align: right;
  max-width: 65ch;
  margin: 1rem auto;
}
@media print {
  .pageAnchor-right {
    display: none;
  }
}



/* Prism - is used for code highlighting from an external pen: https://codepen.io/2kool2/pen/MEbeEg */

figure {
  max-width: 65ch;
  margin: 2rem auto;
}
figure + figure {
  margin-top: 3rem;
}
figure + .main_p {
  margin-top: 2rem;
}
figcaption {
  font-weight: 700;
  padding: 0 0 0 .5rem;
}
pre,
pre > code {
  background-color: transparent !important;
}
pre:focus {
  outline: #fff solid .125rem;
}
pre > code {
  display:block;
  margin: 0 0 0.75rem;
  padding-left: 0.5rem;
}
pre[class*="language-"] {
  background-position-x: -0.3rem;
}
code[class*="language-"] {
  line-height: 2rem;
}


/* Fancier text links */
body {
  --fancyColor: hsla(214,72%,48%,1);
  --fancyColorHover: hsla(214,72%,20%,1);
  --fancyBgHover: hsla(214,100%,80%,1);
  --fancyUnderline: hsla(214,72%,48%,1);
}
@media (prefers-color-scheme: dark) {
  body:not([data-lightMode="light"]) {
    --fancyColor: hsla(214,100%,72%,1);
    --fancyColorHover: #fff;
    --fancyBgHover: hsla(214,100%,25%,1);
    --fancyUnderline: hsla(214,100%,72%,.4);
  }
}
body[data-lightMode="dark"] {
  --fancyColor: hsla(214,100%,72%,1);
  --fancyColorHover: #fff;
  --fancyBgHover: hsla(214,100%,25%,1);
  --fancyUnderline: hsla(214,100%,72%,.4);
}
/* Fancy links */
.fancyLnk:link,
.fancyLnk:visited {
  color: var(--fancyColor);
  white-space: nowrap;
  position: relative;
  text-decoration: none;
  border-bottom: 1px solid var(--fancyUnderline);
  transition: color .3s ease-out;
}
.fancyLnk:hover,
.fancyLnk:focus {
  color: var(--fancyColorHover);
}
.fancyLnk:focus {
  outline: none;
}
.fancyLnk::before,
.fancyLnk::after {
  content: '';
  position: absolute;
  left: 0;
  width: 100%;
  opacity: .5;
  transform: scaleX(0);
  transition: none;
  /* Set to the delayed-on time, or as close as possible */
  transition: all .15s linear;
}
.fancyLnk::before {
  background-color: var(--fancyBgHover);
  outline-offset: -.25rem;
  outline: var(--fancyBgHover) solid .5rem;
  height: 100%;
  bottom: 0;
  z-index: -1;
}
.fancyLnk::after {
  background-color: var(--fancyColor);
  bottom: -0.1rem;
  height: 0.1rem;
}
.fancyLnk:hover::before,
.fancyLnk:focus::before,
.fancyLnk:hover::after,
.fancyLnk:focus::after {
  opacity: 1;
  transform: scaleX(1);
  /* Delayed to reduce annoying unintentional activations */
  transition: all .3s ease-in .15s;
}
@media (prefers-reduced-motion: reduce) {
  .fancyLnk::before,
  .fancyLnk::after {
    transition: none;
  }
}
// Add tabindex, to target, on clicking an in-page anchor
//   - Remove tabindex on bluring the target element.
const xxxinPageAnchors = (_ => {
  return;
  'use strict'
  const currentURL = self.location.href;

  const scrollAndFocus = target => {
    target.setAttribute('tabindex', '-1');
    target.addEventListener('blur', _ => target.removeAttribute('tabindex'), {once: true});
    requestAnimationFrame(_ => {
      target.scrollIntoView();
      target.focus({preventScroll:true});
    });
  };

  const anchorClicked = e => {
    const a = e.target;
    const to_id = a.hash.split('#')[1];
    const to_obj = document.getElementById(to_id);

    // If invalid return retaining default behaviour.
    if (!to_obj) return;

    e.preventDefault();
    
    // Update the URL fragment identifier
    // - so the back button works!
    window.location.hash = to_id;

    scrollAndFocus(to_obj);
  };

  const anchors = document.getElementsByTagName('a');
  for (const a of anchors) {

    // Filter for in-page anchors only!
    if (!a.hash) continue;
    if (a.origin + a.pathname !== self.location.origin + self.location.pathname) continue;

    a.addEventListener('click', anchorClicked);
  }

})();

// UglifyJS 3 ES6 - 532 bytes
const inPageAnchors=(_=>{"use strict";self.location.href;const t=e=>{const t=e.target.hash.split("#")[1],n=document.getElementById(t);n&&(e.preventDefault(),window.location.hash=t,(e=>{e.setAttribute("tabindex","-1"),e.addEventListener("blur",t=>e.removeAttribute("tabindex"),{once:!0}),requestAnimationFrame(t=>{e.scrollIntoView(),e.focus({preventScroll:!0})})})(n))},n=document.getElementsByTagName("a");for(const e of n)e.hash&&e.origin+e.pathname===self.location.origin+self.location.pathname&&e.addEventListener("click",t)})();


// Google compiled ES5 989 bytes
//var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(a){var c=0;return function(){return c<a.length?{done:!1,value:a[c++]}:{done:!0}}};$jscomp.arrayIterator=function(a){return{next:$jscomp.arrayIteratorImpl(a)}};$jscomp.makeIterator=function(a){var c="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return c?c.call(a):$jscomp.arrayIterator(a)};var inPageAnchors=function(a){var c=function(a){a.setAttribute("tabindex","-1");a.addEventListener("blur",function(b){return a.removeAttribute("tabindex")},{once:!0});requestAnimationFrame(function(b){a.scrollIntoView();a.focus({preventScroll:!0})})};a=function(a){var b=a.target.hash.split("#")[1],d=document.getElementById(b);d&&(a.preventDefault(),window.location.hash=b,c(d))};var d=document.getElementsByTagName("a");d=$jscomp.makeIterator(d);for(var b=d.next();!b.done;b=d.next())b=b.value,b.hash&&b.origin+b.pathname===self.location.origin+self.location.pathname&&b.addEventListener("click",a)}();



/* Prism - is used for code highlighting from an external pen: https://codepen.io/2kool2/pen/MEbeEg */
// Force Prism to use light mode 
document.body.setAttribute('data-lightmode', 'light');
Run Pen

External CSS

  1. https://codepen.io/2kool2/pen/MEbeEg.css

External JavaScript

  1. https://codepen.io/2kool2/pen/MEbeEg.js