Skip to main content

A material design inspired show & hide.
Very accessible, vanilla JavaScript tasty goodness no?

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".

Language HTML
<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.

Language HTML
<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.

Language CSS
.md-spot,
.hidden {
   transform: translate3d(0,0,0);
   backface-visibility: hidden;
   perspective: 1000;
}

The containing block class="pab".

Language CSS
.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.

Language CSS
.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.

Language CSS
.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.

Language CSS
.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.

Language CSS
.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).

Language CSS
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.

Socialise: