<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>
<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>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><div id="top"></div>
<a class="skip_lnk-content" href="#main">Main content</a>
<!-- Nav, etc -->
<!-- In-page anchors -->
<a href="#code">Code</a>
<a href="#credits">Credits</a>
<main id="main">
<section id="code">
<!-- May contain focusable elements -->
</section>
<section>
<!-- Personally, I prefer to link to the heading -->
<h2 id="credits">Credits</h2>
<!-- May contain focusable elements -->
</section>
<a href="#top">Back to top</a>
</main>
</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 = (_ => {
// 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);
}
})();
</code></pre>
</figure>
<figure>
<figcaption>JavaScript ES6 <small>(UglifyJS 3 - 532 bytes)</small></figcaption>
<pre><code class=language-javascript spellcheck=false contenteditable>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)})();
</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<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)}();
</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>
[[[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 {
min-height: 0vw;
--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_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;
}
}
.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 {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;
}
[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}
.pageAnchor-right {
text-align: right;
max-width: 65ch;
margin: 1rem auto;
}
@media print {
.pageAnchor-right {
display: none;
}
}
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;
}
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);
}
.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;
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);
transition: all .3s ease-in .15s;
}
@media (prefers-reduced-motion: reduce) {
.fancyLnk::before,
.fancyLnk::after {
transition: none;
}
}
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 (!to_obj) return;
e.preventDefault();
window.location.hash = to_id;
scrollAndFocus(to_obj);
};
const anchors = document.getElementsByTagName('a');
for (const a of anchors) {
if (!a.hash) continue;
if (a.origin + a.pathname !== self.location.origin + self.location.pathname) continue;
a.addEventListener('click', anchorClicked);
}
})();
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)})();
document.body.setAttribute('data-lightmode', 'light');