<a class=skip_lnk href="#main_h1">Skip to content</a>
<nav class=fakeNav>
<ul class=fakeNav_lst>
<li class=fakeNav_li><a class=fakeNav_lnk href>dummy nav link</a></li>
<li class=fakeNav_li><a class=fakeNav_lnk href>dummy nav link</a></li>
</ul>
</nav>
<main class=main>
<!-- tabindex = -1 is required for iOS with JS unavailable -->
<h1 class=main_h1 id=main_h1>Accessible skip-to-content link for desktop and mobile</h1>
<p class=main_p>Assistive technologies struggle with skip-links on mobile and touch devices.</p>
<p class=main_p>Activating the skip-link anchor (only available via tab key) moves the focus to its <code class="main_code language-html">href</code> which is problematic on iOS and Android devices.</p>
<p class=main_p>I've written a futherence to this piece to include accessible <a class=fancyLnk rel="me noopener" target=_blank title="[new window]" href="https://codepen.io/2kool2/pen/JjYGdmZ">In-page anchors for desktop & mobile</a> (includes skip-to-content).</p>
<p class=main_p>These solutions correct experienced issues across desktop, iOS and Android.</p>
<h2 class=main_h2>Skip-link to the content heading</h2>
<figure>
<figcaption>HTML</figcaption>
<pre><code class=language-markup spellcheck=false contenteditable><a class=skip_lnk href="#main_title">Skip to main content</a>
<!-- Nav, etc -->
<h1 class="main_title" id="main_title" tabindex="-1">Page heading</h1>
</code></pre>
</figure>
<p class=main_p><code class="main_code language-html">tabindex="-1"</code> resolves iOS issues.</p>
<p class=main_p>Note: I'm using the <code class="main_code language-html">h1</code> as the focus point.<br>Wherever possible avoid moving focus onto blocks of content, such as <code class="main_code language-html">main</code>, <strong>especially</strong> if it contains focusable elements: links, inputs, video, etc.</p>
<figure>
<figcaption>CSS</figcaption>
<pre><code class=language-css spellcheck=false contenteditable>.skip_lnk {
position: absolute;
padding: .5rem 1rem;
color: #fff;
background-color: #000;
text-decoration: none;
font-weight: bold;
z-index: 10;
transform: translate3d(.125rem, -5rem, 0);
transition: transform .3s ease-out;
}
.skip_lnk:focus {
transform: translate3d(.125rem, .125rem, 0);
outline: #fff solid .125rem;
}
@media print {
.skip_lnk {
display: none;
}
}
</code></pre>
</figure>
<p class=main_p>JavaScript is required to resolve Android issues.</p>
<figure>
<figcaption>JavaScript (ES6)</figcaption>
<pre><code class=language-javascript spellcheck=false contenteditable>(() => {
const skip_lnk = document.querySelector('.skip_lnk');
if (!skip_lnk) return false;
skip_lnk.addEventListener('click', (e) => {
e.preventDefault();
const to_obj = document.getElementById(skip_lnk.href.split('#')[1]);
if (to_obj) to_obj.focus();
});
})();
</code></pre>
</figure>
<figure>
<figcaption>JavaScript (ES5) - Google closure compiled 194 bytes</figcaption>
<pre><code class=language-javascript spellcheck=false contenteditable>(function(){var a=document.querySelector(".skip_lnk");if(!a)return!1;a.addEventListener("click",function(b){b.preventDefault();(b=document.getElementById(a.href.split("#")[1]))&&b.focus()})})();
</code></pre>
</figure>
<h2 class=main_h2>Skip-link to a content block</h2>
<p class=main_p>If you cannot link to the main heading, and have to link to a content block (which may contain focusable elements), then avoid the use of <code class="main_code language-html">tabindex</code> in the HTML.</p>
<p class=main_p>Instead add and remove it upon demand via JavaScript.</p>
<figure>
<figcaption>HTML</figcaption>
<pre><code class=language-markup spellcheck=false contenteditable><a class=skip_lnk href="#main">Skip to main content</a>
<!-- Nav, etc -->
<main class="main" id="main">
<!-- May contain focusable elements -->
</main>
</code></pre>
</figure>
<figure>
<figcaption>JavaScript (ES6)</figcaption>
<pre><code class=language-javascript spellcheck=false contenteditable>(_ => {
const skip_lnk = document.querySelector('.skip_lnk');
if (!skip_lnk) return false;
skip_lnk.addEventListener('click', e => {
e.preventDefault();
const to_obj = document.getElementById(skip_lnk.href.split('#')[1]);
if (to_obj) {
to_obj.setAttribute('tabindex', '-1');
to_obj.addEventListener('blur', e => {
to_obj.removeAttribute('tabindex');
}, {once: true});
to_obj.focus();
}
});
})();
</code></pre>
</figure>
<p class=main_p>For improved accessibility, the on-demand addition and removal of <code class="main_code language-html">tabindex</code> should be applied to <strong>all page anchors</strong>, not just skip-to-content links.</p>
<p class=main_p>I've rewritten this piece to do exactly that in an accessible manner: <a class=fancyLnk rel="me noopener" target=_blank title="[new window]" href="https://codepen.io/2kool2/pen/JjYGdmZ">In-page anchors for desktop & mobile</a></p>
<h2 class=main_h2>Credits</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>.<br>The code here replaces the article's jQuery version with vanilla JavaScript and was extended to include on-demand tabindexing.</p>
</main>
<!-- Footer codepen include -->
[[[https://codepen.io/2kool2/pen/mKeeGM]]]
html {box-sizing: border-box;}
*,
*::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 {
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:focus {
transform: translate3d(.125rem, .125rem, 0);
outline: #fff solid .125rem;
}
@media print {
.skip_lnk {
display: none;
}
}
/* Fake nav */
.fakeNav {
color: #fff;
background-color: #463b4c;
border: 2px solid #463b4c;
}
.fakeNav_lst {
margin: 0;
padding: 0;
display: flex;
justify-content: space-around;
list-style: none;
}
.fakeNav_li {}
.fakeNav_lnk {
display: block;
font-weight: bold;
color: #fff;
padding: .5em;
}
.fakeNav_lnk:focus {
outline: #fff solid .125rem;
}
/* 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;
}
/* Just so you can see where the focus lands */
[tabindex="-1"]:focus {
color: red;
outline: #fff solid .125rem;
}
.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}
/* Prism - is used for code highlighting from an external pen: https://codepen.io/2kool2/pen/MEbeEg */
figure {
max-width: 65ch;
margin: 1.5rem auto;
}
figure + figure {
margin-top: 2rem;
}
figcaption {
font-weight: 700;
padding: 0 0 0 .5rem;
}
pre[class*="language-"] {
/* background-position: top -1.2rem left -.25rem; */
}
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;
}
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);
}
.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;
}
}
// JS required to resolve Android issues
// (_ => {
// const skip_lnk = document.querySelector('.skip_lnk');
// if (!skip_lnk) return false;
// skip_lnk.addEventListener('click', e => {
// e.preventDefault();
// const to_obj = document.getElementById(skip_lnk.href.split('#')[1]);
// if (to_obj) to_obj.focus();
// });
// })();
// ES5 version
/*
var skip_to_content_link = (function () {
var skip_lnk = document.querySelector('.skip_lnk');
if (!skip_lnk) return false;
skip_lnk.addEventListener('click', function(e) {
e.preventDefault();
var to_obj = document.getElementById(skip_lnk.href.split('#')[1]);
if (to_obj) to_obj.focus();
});
})();
*/
// ES5 version - Google closure compiled 175 bytes gzipped (194 bytes uncompressed)
/*
(function(){var a=document.querySelector(".skip_lnk");if(!a)return!1;a.addEventListener("click",function(b){b.preventDefault();(b=document.getElementById(a.href.split("#")[1]))&&b.focus()})})();
*/
// ES6 version for content blocks (writes tabindex on demand)
(_ => {
const skip_lnk = document.querySelector('.skip_lnk');
if (!skip_lnk) return false;
skip_lnk.addEventListener('click', e => {
e.preventDefault();
const to_obj = document.getElementById(skip_lnk.href.split('#')[1]);
if (to_obj) {
to_obj.setAttribute('tabindex', '-1');
to_obj.addEventListener('blur', _ => {
to_obj.removeAttribute('tabindex');
}, {once: true});
to_obj.focus();
}
});
})();
/* 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');