Jump to main content

Revealing Captions for Everyone

  • Published:
  • Category: Accessibility

Revealing content on hovering over an element is a great way to add extra interactivity and intrigue to otherwise static content. There have been many great examples, most notably from Codrops on how to achieve this sort of effect in visually creative ways.

However there is a recurring issue with many implementations of this pattern; the only way to reveal the hidden content is solely on hovering with a mouse.

What about keyboard focus or users on touch devices?

The following four examples have been created to showcase a few potential hover effects, while also including focus state effects that either match the hover state, or at least provide a comparable experience in instances where the hover state can’t be replicated exactly on focus. We will cover why there are discrepancies later on.

Hover & Focus Examples

The following iFrame contains the GitHub example page for the hover & focus examples.

See the underlying code of these examples and four more by downloading the source on GitHub.

The Markup

Building the markup for these effects is pretty straight forward, and generally the same for each example.

<figure class="img-block img-block--1" aria-describedby="figcaption-1">
  <img src="img.jpg" class="img-block__img" alt="" />
  <button class="btn img-block__btn">
    <!-- CTA -->
  <figcaption class="img-block__caption" id="figcaption-1">
    <p class="img-block__title">
      <!-- Title (use a heading here if semantically applicable) -->
    <p class="img-block__text">
      <!-- descriptive text -->

A few things to make note of

  • In the instances where an optional link or button is not used, the <figure> should have a tabindex="0" attribute set to it, so that the content is focusable and thus the caption can be revealed.
  • An alt attribute should always be set on an image element, however there are occasions where alt text should not be added.

    For example, in these instances the images are contained within <figure>s with <figcaption>s.

    Since the captions are being used to provide information about their paired images, the images should have empty alt attributes otherwise screen readers would announce potentially repetitive content, which could be perceived as confusing or annoying.

  • Adding the aria-describedby attribute to each figure grouping allows screen readers to announce the information within the figure grouping. One might think that the optimal place for the aria-describedby label would be on the image, especially since the alt text attribute is empty. However the attribute has been added to the parent <figure> element for a few reasons.
    1. Internet Explorer still hasn't implemented the accessibility information for the <figure> + <figcaption> elements, so adding the attribute will help create that relationship in IE.
    2. Additionally these demos are styled with an opacity of zero on the initial, hidden state of the captions. Elements with zero opacity are not always read by screen readers. By adding the aria-describedby attribute, the captions will be read, regardless of their opacity level.
    3. Finally, when adding the aria-describedby attribute to the image element, Voice Over, used with Firefox and Chrome on OSX read the contents of the figcaption twice, along with FireFox voice over also adding in random "object replacement character" declarations between the end of figcaption, and the announcement of the button element.

      By placing the aria-describedby on the parent figure element, all the information is accurately announced in Voice Over, without duplication or the addition of undesired "object replacement character" announcements.


The following CSS sets up the base styling for each of the examples.

.img-block {
  display: inline-block;
  margin: 0;
  position: relative;

.img-block:hover .img-block__caption,
.img-block:focus .img-block__caption {
  opacity: 1;

.img-block__img {
  display: block;
  width: 100%;

.img-block__caption {
  background: rgba(150, 30, 120,.8);
  color: #fafafa;
  height: 100%;
  left: 0;
  opacity: 0;
  padding: 20px;
  position: absolute;
  top: 0;
  transition: opacity .2s, transform .2s, padding .2s;
  width: 100%;
  z-index: 2;

@media screen and (max-width: 500px) {
 .img-block__caption {
    padding: 10px;

.img-block__title {
  font-size: 1.2em;

.img-block__btn {
  bottom: 20px;
  position: absolute;
  right: 20px;
  transform: scale(1);
  transition: transform .1s;
  z-index: 5;

.img-block__text {
  font-size: .8em;

.img-block__text a {
  color: #fff;
  font-weight: bold;

Check out the CSS source file to see the rules for the individual example transitions.

When viewing the CSS, you’ll notice that there is a selector for both the :focus and :hover states of the .img-block. However, the only way the focus state will fire is if a tabindex attribute is added to the <figure> element (which has been done in examples 3 and 4).

While applying a tabindex to all the examples would create a focus state that matched the hover state, doing so would then make any caption that then also contained focusable content (like the button) have two focus states. One for the initial focus of the figure, and a second, different state, for focusable content within the figure caption.

I’d highly recommend only one or the other if your caption is going to contain focusable content.

Looking back at the markup, the placement the <button> is particularly important in the CSS. Due to the current limitations of the cascade, we absolutely need the button to be placed before the caption, so that we can use the adjacent sibling selector to make the caption viewable on focus of the button. By placing the button inside the caption, or after it, there would be no way for us to show the caption in a CSS only solution.

Potential Issues

The specific markup requirements reveals the first of potential accessibility issues with interactions like these. If you need to place any links or focusable content within the caption of these examples, you will need to write some JavaScript to maintain the visibility of your content as CSS only solution will no longer be suitable.

To solve this, the following script looks for focusable content within the caption, with a class of js-focus. When focused, a style attribute is added to the figcaption, keeping the content in view, and once focus is lost, the style attribute is removed.

var exLink = document.querySelectorAll(".js-focus");
var i;

for ( i = 0; i &lt; exLink.length; i++ ) {
  exLink[i].addEventListener("focus", function () {
    this.parentNode.parentNode.setAttribute("style", "opacity: 1; z-index: 4");

  exLink[i].addEventListener("blur", function () {

An issue of greater concern is that these interactions don’t really work on mobile. There is no hover, and focus happens on touch of certain elements, in this case the button. That doesn’t really help though, since outside of these demos the buttons should actually be hooked up to do something or navigate somewhere, so they can’t be relied on as a means to provide focus to the figures, especially since they may not always be included in the groupings.

With that in mind and to take touch screens into account, the following script is used to add a class to a top level parent node on the page (in this case, <div id="page">).

// function to see if the current device is a touch device.
function is_touch_device() {
  return ( ('ontouchstart' in window)
           || (navigator.MaxTouchPoints > 0)
           || (navigator.msMaxTouchPoints > 0)

if ( is_touch_device() ) {

See the original post about the above script here: http://ctrlq.org/code/19616-detect-touch-screen-javascript

With the class in place, the following CSS undoes many of the initial styling of the caption area, such as undoing the opacity and positioning, while also negating hover and focus styles, so there aren’t any weird transition hiccups in the instance that a button is pressed, and the transition kicks in, while the page is being navigated away from.

.if-touch-device .btn {
  padding: 8px 12px;

.if-touch-device .img-block__caption,
.if-touch-device .img-block:hover .img-block__caption {
  box-shadow: none;
  height: auto;
  opacity: 1;
  padding: 12px 12px 64px;
  position: static;
  transform: none;

.if-touch-device .img-block:hover:after,
.if-touch-device .img-block:hover:before {
  display: none;

.if-touch-device .img-block:hover .img-block__img {
  box-shadow: none;
  transform: none;

By using the function to detect if the device in use is a touch device, and only adding the above class is the device is indeed a touch device, we will not force touch device styling and interactions on those that have resized their browsers to smaller sizes. This is particularly important since touch devices don’t just include smaller phone viewports, but also tablets.

In closing

You can do a ton with creating CSS only transitions and animations to add extra delight to the presentation of your content. Just be sure to take care that regardless of how people access your content, it can actually be viewed by everyone. Fewer and fewer people expect experiences to be exactly the same across devices or browsers, so don’t let that discourage you. It’s far more important that everyone can access your content than it is for everyone to see it zoom into view.