Accessibly show & hide blocks of content.
Most of the hidden sections on this site are either lists of links or technical content code blocks and make use of the technique described here.
Note this is not an accordion (one drawer open at a time), though modification should be possible.
Mark-up
Mark a container with class="pab"
as a hook for the script to find.
The container must contain both a heading and a container marked class="hidden"
.
<div class=pab>
<h2>Title text</h2>
<div class=hidden>
<p>The hidden content.</p>
</div>
</div>
The script builds on the core markup retaining the key semantic structure.
Replacing the heading content with an activating button.
SVG icon and ARIA roles are also added.
<div class=pab>
<h2 class=btn-added>
<button aria-pressed=false class=pab-btn id=b_0 aria-controls=title_text>
<svg class=svg-plus width=38 height=38 viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg">
<title>Show</title>
<path d="M10.5 19l17 0"></path>
<path class="h" d="M19 10.5l0 17"></path>
</svg>
Title text
<span id=s_0 class=md-spot></span>
</button>
</h2>
<div aria-expanded=false role=region id=title_text class="hidden content-display">
<p>The hidden content.</p>
</div>
</div>
Once initialised the script changes the states of the HTML attributes to be picked up by CSS.
CSS
Hardware acceleration, not needed but never hurts.
.md-spot,
.hidden {
transform: translate3d(0,0,0);
backface-visibility: hidden;
perspective: 1000;
}
The containing block class="pab"
.
.pab {
overflow: hidden;
margin:1.618rem 0;
border: 1px solid rgba(0,0,0,.1);
position: relative;
background-color: #fafafa;
transition:
box-shadow 300ms ease-out 0s,
background-color 300ms ease-out 0s;
}
/* Box shadow used as Safari has
issues with filter drop-shadows */
.pab.hovered {
background-color: #fefefe;
box-shadow: 0 4px 4px rgba(0,0,0,0.3);
}
.pab h2 {
padding:0;
font-size: 1.115rem;
margin: 0;
}
/* No JavaScript available */
.noJS .pab h2 {
padding: 0.618rem 1rem;
}
The activation button.
.pab-btn {
display: block;
cursor: pointer;
width: 100%;
text-align: left;
position: relative;
overflow: hidden;
border: 0;
background-color: transparent;
padding: 0.618rem 0.236rem 0.618rem 2.618rem;
font-size: inherit;
}
.pab-btn:focus {outline: 0 solid}
.pab-btn:hover,
.pab-btn:focus {color:#236ECE}
The SVG animated plus/minus icon.
.svg-plus {
width: 1.8rem;
height: 1.8rem;
position: absolute;
left: 0.6rem;
top: calc(50% - 0.7rem);
vertical-align: middle;
display: inline-block;
margin: -0.236rem 0.236rem 0 0;
transition: transform 0.7s ease-out 0s;
}
.svg-plus path {
stroke-width: 5;
stroke-linecap: square;
stroke: #aaa;
transition:
stroke 0.5s ease-out 0s,
opacity 0.7s ease-out 0s;
}
.pab-btn.ON .svg-plus {
transform: rotateZ(360deg);
}
.pab-btn.ON .h {opacity: 0;}
.pab-btn:hover .svg-plus path,
.pab-btn:focus .svg-plus path {stroke:#418cec}
The clicked spot animation.
.md-spot {
display: block;
position: absolute;
background-color: rgba(35, 110, 206, 0.35);
border-radius: 50%;
transform: scale(0);
opacity: 1;
filter: blur(4rem);
}
.md-spot.animateIn {
animation: md-spot-open 0.65s ease-in;
}
@keyframes md-spot-open {
100% {
opacity: 0;
transform: scale(2.4);
}
}
.md-spot.animateOut {
animation: md-spot-close 0.65s ease-out;
}
@keyframes md-spot-close {
0% {
opacity: 0;
transform: scale(2.4);
}
100% {
opacity: 1;
transform: scale(0);
}
}
The hidden content container.
.hidden {
border-top: 1px solid rgba(0,0,0,.1);
padding: 0 1rem;
margin: 0;
max-height: 0;
transition: max-height 0.65s ease-in-out;
overflow: hidden;
background-color: #fcfcfc;
}
/* .hidden has this class added
to remove content from the
keyboard chain */
.content-display {
display: none;
}
/* No JavaScript available */
.noJS .hidden {max-height:none;}
The only thing to avoid is vertical margin, or padding, applied directly to the hidden container.
Google closure compiled
v5.5a - 1.35KB gzipped (3.5KB uncompressed).
var d = document;
var peekaboo=function(){function x(b,a){var c,e,f,g;a.f.classList.remove("animateIn","animateOut");c=a.f.clientWidth;e=a.f.clientHeight;c||e||(h=a.clientWidth);f=b.offsetX-h/2;g=b.offsetY-h/2;window.requestAnimationFrame(function(){a.f.setAttribute("style","top: "+g+"px; left: "+f+"px; height: "+h+"px; width: "+h+"px");a.a.classList.contains("ON")?a.f.classList.add("animateIn"):a.f.classList.add("animateOut")})}function y(b){var a=b.cloneNode(!0),c=0;a.setAttribute("style","display:block; width:"+b.clientWidth+
"px; position:absolute; top:0; left:-999rem; max-height:none; visibility:hidden;");b.parentElement.appendChild(a);c=a.clientHeight;b.parentElement.removeChild(a);return c}function l(b,a){var c=b.getElementsByTagName("title");c&&(c[0].innerHTML=a?"Hide":"Show");b.setAttribute("aria-pressed",a);b.b.setAttribute("aria-expanded",a)}function r(b){clearTimeout(b.b.g);b.a.classList.contains("ON")?(window.requestAnimationFrame(function(){b.b.setAttribute("style","max-height:0");b.b.classList.remove("ON");
b.a.classList.remove("ON")}),b.b.g=setTimeout(function(){window.requestAnimationFrame(function(){b.b.classList.add("content-display");l(b.a,!1);var a=b.b.id;useLocalStorage&&localStorage.removeItem(a)})},700)):(b.b.classList.remove("content-display"),window.requestAnimationFrame(function(){b.b.setAttribute("style","max-height:"+y(b.b)+"px");b.b.classList.add("ON");b.a.classList.add("ON");l(b.a,!0);var a=b.b.id,c=b.a.id;useLocalStorage&&(c?localStorage[a]=c:localStorage.removeItem(a))}))}function z(b){r(this.c);
x(b,this.c)}function t(){this.c.classList.add("hovered")}function u(){this.c.classList.remove("hovered")}function v(b,a,q){var e,f,g,h,k,m,n,p=b.cloneNode(!0);a=-1<a?a:c.length-1;(g=p.getElementsByTagName("h2"))&&(g=g[0]);(f=p.getElementsByClassName("hidden"))&&(f=f[0]);g&&f&&(h=f.classList.contains("ON"),m=f.id?f.id:"r_"+a,k="b_"+a,n="s_"+a,f.id=m,f.classList.add("content-display"),f.setAttribute("role","region"),e=d.createElement("button"),e.setAttribute("aria-controls",f.id),e.id=k,e.className=
"pab-btn",h&&e.classList.add("ON"),e.innerHTML='<svg class=svg-plus width=38 height=38 viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg"><title>Show</title><path d="M10.5 19l17 0"/><path class=h d="M19 10.5l0 17"/></svg>',e.innerHTML+=g.innerHTML,e.innerHTML+="<span id="+n+" class=md-spot></span>",g.innerHTML="",g.classList.add("btn-added"),g.appendChild(e),requestAnimationFrame(function(){b.parentNode.replaceChild(p,b);c[a].a=d.getElementById(k);c[a].b=d.getElementById(m);c[a].f=d.getElementById(n);
c[a].a.b=c[a].b;c[a].a.c=c[a];c[a].b.a=c[a].a;c[a].b.c=c[a];l(c[a].a,h);c[a].a.addEventListener("click",z,!1);c[a].a.addEventListener("mouseover",t,!1);c[a].a.addEventListener("mouseout",u,!1);c[a].a.addEventListener("focus",t,!1);c[a].a.addEventListener("blur",u,!1);q&&q(c[a].a);return c[a].a}))}function w(){for(var b=d.getElementsByClassName("ON hidden"),a=b.length;a--;)b[a].a.classList.remove("ON"),r(b[a].c)}function A(){k=clearTimeout(k);k=setTimeout(w,350)}function B(){useLocalStorage&&setTimeout(function(){var b,
a,c;for(b=0;b<localStorage.length;b++)a=localStorage.key(b),c=localStorage.getItem(a),a=d.getElementById(a),c=d.getElementById(c),a&&c&&(c.classList.add("ON"),a.classList.add("ON"),l(c,!0),w())},700)}var c=d.getElementsByClassName("pab"),h,k;return{h:function(){var b=c.length;if(document.addEventListener&&d.getElementsByClassName&&document.documentElement.classList&&window.requestAnimationFrame){for(;b--;)v(c[b],b,!1);window.addEventListener("resize",A,!0);B()}},add:v}}();
peekaboo.h();
// Dynamically add a new pab block
peekaboo.add(obj);
Fully annotated script is available in the demo source.
Still to do
Change from using on-click to on-touch-end to improve responsiveness on touch devices.
Social links and email client: