Accessibility pattern · Overlays

Image lightbox

Each thumbnail is a button named by its picture's alt text, and opens a modal dialog with the picture large, its caption and its place in the set. Previous, Next and the arrow keys move through the set, a polite live region says which picture is showing, and Escape returns focus to the thumbnail.

WCAG criteria
7
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Kerala in six pictures

Choose a picture to see it larger, with its caption.

Kolukkumalai at dawn. The last stretch up is a jeep track, so book a ride the evening before.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Enter or SpaceOn a thumbnail, opens the viewer at that picture, with focus on Next.
Arrow Right or Arrow LeftIn the viewer, shows the next or previous picture, wrapping at the ends.
Home or EndIn the viewer, shows the first or last picture.
Tab or ShiftTabMoves through the viewer's buttons only; the page behind is inert while it is open.
EscapeCloses the viewer and returns focus to the thumbnail that opened it.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches the third thumbnailThe silhouette of a large cantilevered fishing net against an orange sunset over the sea, button, opens a dialog
The viewer opensFishing nets at Fort Kochi, dialog, Image 3 of 6. Next, button
Next or Arrow Right shows the next pictureCliffs at Varkala, Image 4 of 6
Escape closes the viewerThe silhouette of a large cantilevered fishing net against an orange sunset over the sea, button (focus is back on the thumbnail)

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-lightbox" data-ap-lightbox>
  <div class="ap-lightbox__top">
    <h3 class="ap-lightbox__album">Kerala in six pictures</h3>
    <p class="ap-lightbox__hint">Choose a picture to see it larger, with its caption.</p>
  </div>

  <!-- Gradients the pictures share; drawn once, used by the thumbnails and the viewer. -->
  <svg class="ap-lightbox__defs" aria-hidden="true" focusable="false" width="0" height="0">
    <defs>
      <linearGradient id="lightbox-sky1" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#BDE2F0"/><stop offset="1" stop-color="#FBEBCB"/></linearGradient>
      <linearGradient id="lightbox-mist" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#FFFFFF" stop-opacity="0"/><stop offset=".5" stop-color="#FFFFFF" stop-opacity=".6"/><stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/></linearGradient>
      <linearGradient id="lightbox-sky2" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#EF9068"/><stop offset=".6" stop-color="#F7C68A"/><stop offset="1" stop-color="#FCE3B0"/></linearGradient>
      <linearGradient id="lightbox-water2" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#4F8F8C"/><stop offset="1" stop-color="#1D4D52"/></linearGradient>
      <linearGradient id="lightbox-sky3" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#3A2462"/><stop offset=".5" stop-color="#B9496E"/><stop offset=".8" stop-color="#F08A5D"/><stop offset="1" stop-color="#FFCB80"/></linearGradient>
      <linearGradient id="lightbox-sea3" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#5B2E5F"/><stop offset="1" stop-color="#20132F"/></linearGradient>
      <linearGradient id="lightbox-sky4" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#6CC0E8"/><stop offset="1" stop-color="#DDF3FB"/></linearGradient>
      <linearGradient id="lightbox-sea4" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2AA2C8"/><stop offset="1" stop-color="#0F6E95"/></linearGradient>
      <linearGradient id="lightbox-sky5" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#7B90A3"/><stop offset="1" stop-color="#C6D3DC"/></linearGradient>
      <pattern id="lightbox-rain" width="16" height="16" patternUnits="userSpaceOnUse"><path d="M12 0 8 9M4 8 1 15" stroke="#FFFFFF" stroke-width="1" stroke-linecap="round" opacity=".5"/></pattern>
      <linearGradient id="lightbox-sky6" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#141C46"/><stop offset=".45" stop-color="#44357A"/><stop offset=".8" stop-color="#C66F7E"/><stop offset="1" stop-color="#F2A982"/></linearGradient>
      <linearGradient id="lightbox-sea6" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2B3567"/><stop offset="1" stop-color="#141A3D"/></linearGradient>
      <linearGradient id="lightbox-beam" x1="1" y1="0" x2="0" y2="0"><stop offset="0" stop-color="#FFE9A8" stop-opacity=".55"/><stop offset="1" stop-color="#FFE9A8" stop-opacity="0"/></linearGradient>
    </defs>
  </svg>

  <ul class="ap-lightbox__grid">
    <li class="ap-lightbox__item">
      <button type="button" class="ap-lightbox__thumb" aria-haspopup="dialog">
        <svg class="ap-lightbox__art" viewBox="0 0 320 200" role="img" aria-label="Rows of tea bushes curving over green hills, with morning mist in the valley and a pale sun"><rect width="320" height="200" fill="url(#lightbox-sky1)"/><circle cx="238" cy="58" r="20" fill="#FFF7E2"/><path d="M0 108C40 88 72 92 106 100s66-22 110-18 66 22 104 10v108H0Z" fill="#A8CDB2"/><rect y="94" width="320" height="44" fill="url(#lightbox-mist)"/><path d="M0 134c50-20 96-18 140-8s100-16 180-8v82H0Z" fill="#6FA97C"/><path d="M0 142c50-20 96-18 140-8s100-16 180-8M0 150c50-20 96-18 140-8s100-16 180-8" fill="none" stroke="#5B9568" stroke-width="2"/><path d="M118 126v-10M182 120v-12" stroke="#3B6B48" stroke-width="2"/><circle cx="118" cy="111" r="7" fill="#3E7550"/><circle cx="182" cy="104" r="8" fill="#3E7550"/><path d="M0 160c70-20 150-14 210-4s90-8 110-12v56H0Z" fill="#3F8050"/><path d="M-4 172c74-20 154-14 214-4s90-8 114-12M-4 184c74-20 154-14 214-4s90-8 114-12M-4 196c74-20 154-14 214-4s90-8 114-12" fill="none" stroke="#2C6A3D" stroke-width="3" stroke-linecap="round"/></svg>
      </button>
      <span class="ap-lightbox__data" hidden><span data-ap-name>Tea gardens above Munnar</span><span data-ap-caption>Kolukkumalai at dawn. The last stretch up is a jeep track, so book a ride the evening before.</span></span>
    </li>
    <li class="ap-lightbox__item">
      <button type="button" class="ap-lightbox__thumb" aria-haspopup="dialog">
        <svg class="ap-lightbox__art" viewBox="0 0 320 200" role="img" aria-label="A thatched houseboat on calm water at sunset, with coconut palms along the far bank"><rect width="320" height="200" fill="url(#lightbox-sky2)"/><circle cx="92" cy="90" r="18" fill="#FFF2CC"/><rect y="106" width="320" height="94" fill="url(#lightbox-water2)"/><path d="M0 100h320v8H0z" fill="#2E4A36"/><g fill="none" stroke="#24392B" stroke-width="2.5" stroke-linecap="round"><path d="M30 102q-4-20 4-38M34 64q-12-2-22 6M34 64q-8-9-20-8M34 64q9-9 21-5M34 64q12 1 18 10M34 64q1-9-3-15"/><path d="M150 102q3-18-3-34M147 68q-12-2-22 6M147 68q-8-9-20-8M147 68q9-9 21-5M147 68q12 1 18 10"/><path d="M262 102q-3-16 5-30M267 72q-12-2-21 6M267 72q-8-9-19-7M267 72q9-9 20-4M267 72q11 1 17 10"/><path d="M304 102q-2-12 3-22M307 80q-9-1-16 5M307 80q7-7 15-3M307 80q8 1 12 7"/></g><path d="M78 118h28M84 126h16M88 134h8" stroke="#FFE4A8" stroke-width="2" stroke-linecap="round" opacity=".7"/><ellipse cx="231" cy="170" rx="66" ry="6" fill="#143A3E" opacity=".6"/><path d="M166 150q10 14 34 14h60q24 0 36-16z" fill="#4A2E1B"/><path d="M180 150v-12q0-16 24-18h52q24 2 24 18v12z" fill="#C98B4B"/><path d="M196 122v28M212 120v30M228 120v30M244 120v30M260 121v29" stroke="#A86E35" stroke-width="1.5"/><rect x="184" y="139" width="92" height="7" rx="2" fill="#3A2414"/><path d="M190 142.5h8M206 142.5h8M222 142.5h8M238 142.5h8M254 142.5h8" stroke="#F6CC7A" stroke-width="3"/></svg>
      </button>
      <span class="ap-lightbox__data" hidden><span data-ap-name>Houseboat on the backwaters</span><span data-ap-caption>Near Alappuzha, an hour before sunset, when the water goes still.</span></span>
    </li>
    <li class="ap-lightbox__item">
      <button type="button" class="ap-lightbox__thumb" aria-haspopup="dialog">
        <svg class="ap-lightbox__art" viewBox="0 0 320 200" role="img" aria-label="The silhouette of a large cantilevered fishing net against an orange sunset over the sea"><rect width="320" height="200" fill="url(#lightbox-sky3)"/><circle cx="214" cy="134" r="28" fill="#FFE1A0"/><rect y="134" width="320" height="66" fill="url(#lightbox-sea3)"/><path d="M190 144h48M198 152h32M206 160h16" stroke="#FFC98A" stroke-width="2" stroke-linecap="round" opacity=".7"/><path d="M126 106 158 54 192 104q-34 24-66 2z" fill="#1B1029" opacity=".3"/><g fill="none" stroke="#1B1029" stroke-linecap="round"><path d="M70 154 158 54" stroke-width="3.5"/><path d="M70 154 36 112" stroke-width="3"/><path d="M158 54 126 106M158 54 192 104" stroke-width="1.5"/><path d="M126 106q32 22 66-2" stroke-width="2"/><path d="M136 111 158 54M147 115 158 54M168 115 158 54M180 111 158 54" stroke-width=".8"/><path d="M36 112v18M44 122v14M100 120 70 154" stroke-width="1.2"/></g><circle cx="36" cy="133" r="4" fill="#1B1029"/><circle cx="44" cy="139" r="3.5" fill="#1B1029"/><path d="M44 154h60v5H44z" fill="#1B1029"/><path d="M52 159v24M96 159v24" stroke="#1B1029" stroke-width="3"/><g fill="none" stroke="#2A1838" stroke-linecap="round"><path d="M268 150 300 96" stroke-width="2.5"/><path d="M300 96 286 122M300 96 314 120" stroke-width="1"/><path d="M286 122q14 9 28-2" stroke-width="1.2"/></g><path d="M110 40q4-4 8 0 4-4 8 0M136 30q3-3 6 0 3-3 6 0" fill="none" stroke="#1B1029" stroke-width="1.5" stroke-linecap="round"/></svg>
      </button>
      <span class="ap-lightbox__data" hidden><span data-ap-name>Fishing nets at Fort Kochi</span><span data-ap-caption>The nets are raised and lowered by hand, with stones for counterweights.</span></span>
    </li>
    <li class="ap-lightbox__item">
      <button type="button" class="ap-lightbox__thumb" aria-haspopup="dialog">
        <svg class="ap-lightbox__art" viewBox="0 0 320 200" role="img" aria-label="Red laterite cliffs topped with palms, above a sandy beach and a blue sea"><rect width="320" height="200" fill="url(#lightbox-sky4)"/><rect y="96" width="320" height="104" fill="url(#lightbox-sea4)"/><path d="M220 110h20M262 120h14M196 126h10" stroke="#E6F7FD" stroke-width="1.5" stroke-linecap="round" opacity=".8"/><path d="M150 200q22-34 64-38t106 6v32z" fill="#F2D4A2"/><path d="M166 176q30-12 70-10t84 8" fill="none" stroke="#FFFFFF" stroke-width="2.5" stroke-linecap="round" opacity=".85"/><path d="M0 74h118q14 2 18 14l12 58q6 26-8 54H0z" fill="#B5542F"/><path d="M118 74q14 2 18 14l12 58q6 26-8 54h-24q14-30 6-58z" fill="#8E3B22" opacity=".5"/><path d="M0 104h128M0 126h134M0 150h140M0 174h142" stroke="#8F3C22" stroke-width="2" opacity=".55"/><path d="M0 70h116q14 0 20 12l-2 2q-6-8-18-8H0z" fill="#5C9A4E"/><g fill="none" stroke="#2F5B34" stroke-width="2.5" stroke-linecap="round"><path d="M28 72q-3-18 4-32M32 40q-12-2-20 6M32 40q-8-9-19-7M32 40q9-9 20-4M32 40q11 1 17 10"/><path d="M70 72q3-16-3-28M67 44q-11-2-19 5M67 44q-7-8-17-6M67 44q8-8 18-4M67 44q10 1 15 9"/><path d="M104 72q-2-12 3-22M107 50q-9-1-15 5M107 50q7-7 15-3M107 50q8 1 12 7"/></g></svg>
      </button>
      <span class="ap-lightbox__data" hidden><span data-ap-name>Cliffs at Varkala</span><span data-ap-caption>North Cliff in the late morning, from the path along its edge.</span></span>
    </li>
    <li class="ap-lightbox__item">
      <button type="button" class="ap-lightbox__thumb" aria-haspopup="dialog">
        <svg class="ap-lightbox__art" viewBox="0 0 320 200" role="img" aria-label="Bands of green paddy fields under a grey monsoon sky, with rain falling and a canal running through them"><rect width="320" height="200" fill="url(#lightbox-sky5)"/><ellipse cx="80" cy="40" rx="90" ry="26" fill="#6E8396" opacity=".55"/><ellipse cx="240" cy="30" rx="110" ry="28" fill="#62788C" opacity=".5"/><path d="M0 98q20-8 40-2t40-4 40 3 40-5 40 4 40-3 40 4 40-2v16H0z" fill="#3E5B45"/><path d="M0 108h320v20H0z" fill="#93C063"/><path d="M0 128h320v26H0z" fill="#72A94C"/><path d="M0 154h320v24H0z" fill="#A3CE70"/><path d="M0 178h320v22H0z" fill="#5F9B45"/><path d="M0 128h320M0 154h320M0 178h320" stroke="#C8E3A0" stroke-width="1.5" opacity=".7"/><path d="M150 108h10l-58 92H70z" fill="#A9C9D6"/><path d="M106 160q10 4 22 0l-3 4h-16z" fill="#2E3A33"/><rect width="320" height="200" fill="url(#lightbox-rain)"/></svg>
      </button>
      <span class="ap-lightbox__data" hidden><span data-ap-name>Paddy fields in the rain</span><span data-ap-caption>Kuttanad in July. Much of this farmland lies below sea level.</span></span>
    </li>
    <li class="ap-lightbox__item">
      <button type="button" class="ap-lightbox__thumb" aria-haspopup="dialog">
        <svg class="ap-lightbox__art" viewBox="0 0 320 200" role="img" aria-label="A red and white striped lighthouse on dark rocks at dusk, its lamp lit and the first stars out"><rect width="320" height="200" fill="url(#lightbox-sky6)"/><g fill="#FFFFFF"><circle cx="40" cy="24" r="1"/><circle cx="92" cy="14" r="1.2"/><circle cx="150" cy="30" r=".9"/><circle cx="196" cy="12" r="1.1"/><circle cx="276" cy="26" r="1"/><circle cx="300" cy="10" r=".8"/><circle cx="20" cy="56" r=".8"/></g><path d="M230 59 20 26v64z" fill="url(#lightbox-beam)"/><rect y="142" width="320" height="58" fill="url(#lightbox-sea6)"/><path d="M40 156h40M60 166h24M206 160h50" stroke="#F3A981" stroke-width="1.5" stroke-linecap="round" opacity=".5"/><path d="M140 200l18-44q14-20 40-22l38-6q30 2 50 18l34 10v44z" fill="#211A36"/><path d="M221 134l3-68h12l3 68z" fill="#F4EFE6"/><path d="M223.4 82h13.2l.5 12h-14.2zM222.3 106h15.4l.5 12h-16.4z" fill="#C2412D"/><path d="M219 64h22v3h-22z" fill="#2A2340"/><rect x="224" y="54" width="12" height="10" fill="#FFE08A"/><path d="M222 54l8-8 8 8z" fill="#2A2340"/><path d="M206 134h14v-8h-14zM240 134h12v-6h-12z" fill="#2A2340"/></svg>
      </button>
      <span class="ap-lightbox__data" hidden><span data-ap-name>Lighthouse at Kovalam</span><span data-ap-caption>From Lighthouse Beach just after sunset, when the lamp comes on.</span></span>
    </li>
  </ul>

  <dialog class="ap-lightbox__viewer" aria-labelledby="lightbox-name" aria-describedby="lightbox-count">
    <div class="ap-lightbox__sheet">
      <div class="ap-lightbox__bar">
        <div class="ap-lightbox__id" aria-live="off" aria-atomic="true" data-ap-live>
          <h2 class="ap-lightbox__name" id="lightbox-name" data-ap-name-out>Tea gardens above Munnar</h2>
          <p class="ap-lightbox__count" id="lightbox-count"><span>Image</span> <span data-ap-n translate="no">1</span> <span>of</span> <span data-ap-total translate="no">6</span></p>
        </div>
        <button type="button" class="ap-lightbox__close" data-ap-close aria-label="Close">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
        </button>
      </div>
      <div class="ap-lightbox__frame" data-ap-frame></div>
      <p class="ap-lightbox__caption" data-ap-caption-out>Kolukkumalai at dawn. The last stretch up is a jeep track, so book a ride the evening before.</p>
      <p class="ap-lightbox__alt" aria-hidden="true"><span class="ap-lightbox__alt-k">Alt text</span> <span data-ap-alt-out></span></p>
      <div class="ap-lightbox__nav">
        <button type="button" class="ap-btn" data-ap-prev>
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m15 18-6-6 6-6"/></svg>
          Previous
        </button>
        <span class="ap-lightbox__dots" aria-hidden="true" data-ap-dots></span>
        <button type="button" class="ap-btn" data-ap-next autofocus>
          Next
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 18 6-6-6-6"/></svg>
        </button>
      </div>
    </div>
  </dialog>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.1.1 Non-text Content Level A

    Every picture has alt text that says what is in it, and each thumbnail button takes its name from that alt text.

  • 2.1.1 Keyboard Level A

    Opening, moving through the set and closing all work from the keyboard, with buttons for each as well as keys.

  • 2.1.2 No Keyboard Trap Level A

    Focus stays in the viewer while it is open, but Escape and the close button always let it out.

  • 2.4.3 Focus Order Level A

    Focus moves to Next when the viewer opens and back to the thumbnail that opened it when it closes.

  • 2.5.1 Pointer Gestures Level A

    A swipe on the picture moves through the set, and the Previous and Next buttons do the same with a single tap.

  • 4.1.2 Name, Role, Value Level A

    The viewer is a dialog named by the picture's title and described by its place in the set; thumbnails say they open a dialog.

  • 4.1.3 Status Messages Level AA

    The title and Image 4 of 6 sit in a polite live region, so each change of picture is announced without moving focus.

Usage

When to use it

Use it

  • Galleries where people want to see pictures larger, in order, without leaving the page.
  • Product photos, portfolios and trip albums, where the thumbnails alone are too small to judge.

Use something else

  • A single picture: show it at a readable size on the page, with its caption.
  • Charts, maps and diagrams that carry information: give them room on the page and a text alternative.
  • Slides that mix pictures, text and links: use a carousel.

Common failures

How it usually goes wrong

  • Thumbnails that are not buttons

    An image with a click handler cannot be reached with Tab and announces nothing to act on. A button around the picture gives both.

  • Alt text that is a file name

    IMG_2041.jpg or just photo tells nobody what is in the picture, so every thumbnail sounds the same. Describe what it shows.

  • Focus left on the page

    If focus stays behind the viewer, keyboard users tab through hidden thumbnails. showModal() makes the page inert and moves focus in.

  • Focus lost on close

    When the viewer closes and focus jumps to the top, people lose their place in the gallery. It goes back to the thumbnail that opened it.

  • Silent picture changes

    Pressing Next changes the picture on screen, but without a live region a screen reader user hears nothing at all. The title and position are announced.

  • Swipe as the only way

    A gesture-only gallery shuts out keyboard users and anyone who cannot swipe. Buttons and arrow keys do the same job here.

Notes

Building it

  • The viewer opens with showModal(), which makes the page inert and lets Escape close it; the script returns focus to the thumbnail that opened it.
  • The live region is off while the viewer is closed and as it opens, because the dialog's name and description already say which picture it is; from then on, each change is announced once.
  • The visible Alt text line is for sighted readers, so it is hidden from screen readers, which already hear the same words as the picture's name.
  • With real photos, use an img with the same alt in the thumbnail and the viewer, and load the large file only when the viewer opens.
  • The arrow keys are handled on the whole dialog, so they work wherever focus is in it; the viewer has no text field that needs them.

Sources: WAI-ARIA Authoring Practices: Dialog (Modal) · W3C Images Tutorial: Functional images

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction