p
label(for='input-text') Please do not fill this in
input(id='input-text')
p
label(for='input-textarea') Or this one
textarea(id='input-textarea' cols='20' rows='2') I certainly hope this textarea doesn't fall into focus
p
a(href='https://www.bbc.co.uk/' target='_blank') Here's one of those links all the kids go on about
section(id='trap' aria-labelledby='trap-heading')
h1(id='trap-heading') You are now trapped
p
a(href='https://www.bbc.co.uk/' target='_blank') Another link
p Take a doughnut, while you're here. Just one, though.
p 🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩🍩
p
label(for='input-email') I don't care what your email address is
input(id='input-email' type='email')
p
button(type='button' data-js='release-trap') Please release me, let me go
p
button I guess you should submit this
View Compiled
// Just some basic boilerplate
:root {
--dark: #111;
--light: #eee;
--warning: rgb(255 0 0 / 0.2);
}
body {
background: var(--light);
color: var(--dark);
}
@media (prefers-color-scheme: dark) {
body {
background: var(--dark);
color: var(--light);
}
a {
color: #99f;
&:visited {
color: #f9f;
}
}
}
// The focus trap element
section {
background: repeating-linear-gradient(
45deg,
var(--warning),
var(--warning) 1rem,
transparent 1rem,
transparent 2rem
);
border: double 1rem var(--warning);
padding: 1rem;
}
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
button, input, textarea {
display: block;
font-family: inherit;
}
*:focus {
outline: solid 0.2em red;
}
// For the trap buttons
.visually-hidden {
background: transparent;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
View Compiled
const TrapFocus = (passedParent: HTMLElement): void => {
// A string of selectors for elements which can receive focus
const focusSelectors = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, [tabindex]:not([tabindex="-1"])';
// This searches the supplied element for elements which can receive
// focus then returns them in a nodelist.
const getFocusable = (parent: HTMLElement): NodeList | undefined => {
if (!parent) return undefined;
return parent.querySelectorAll(focusSelectors);
};
const parent = passedParent;
// All elements which can receive focus in the DOM
const focusableElements = getFocusable(document.body) as NodeList;
focusableElements.forEach((element: HTMLElement) => {
// Is this an element we need to take out of the focus chain?
if (!parent.contains(element)) {
element.setAttribute('tabindex', '-1');
element.setAttribute('data-trap-focus', 'true');
}
});
const clickHandler = (event: Event) => {
const trigger = event.target as HTMLElement;
const triggerType = trigger.getAttribute('data-js');
// The user wants to turn off the focus trap
if (triggerType && triggerType === 'release-trap') {
// We only want to add focus back in to elements where we've removed it,
// not elements which might have it removed for other reasons
const trappedElements = document.querySelectorAll('[data-trap-focus="true"]') as NodeList;
trappedElements.forEach((element: HTMLElement) => {
element.removeAttribute('tabindex');
element.removeAttribute('data-trap-focus');
});
}
};
// Used for the function which turns off the focus trap
window.addEventListener('click', clickHandler);
};
// The element we want to trap the focus within
const trapElement = document.getElementById('trap');
TrapFocus(trapElement);
View Compiled
This Pen doesn't use any external CSS resources.