Image

An image with a preview overlay, a fallback for a broken source and a placeholder for one still loading.

Accessibility

What Image does for you, and what it leaves to the page around it.

alt does three jobs

alt names the <img>, and with the viewer on it also names the clickable wrapper and the open viewer. Nothing warns you when it is missing.
  • Describe the picture, not the file. “IMG_4821.jpg” or “photo” says nothing, and here it also names a button and a dialog.
  • Give a decorative image alt="". An empty string hides it from screen readers. Leaving alt out entirely makes some screen readers read the URL.
  • A decorative image should not open a viewer. With an empty alt, the wrapper is an unnamed button and the viewer an unnamed dialog. Give it a real name or pass preview={false}.
  • alt stays the same when the fallback shows, so it is all a reader has about the missing picture. Write it for the picture you meant to show.

Reaching the preview from the keyboard

With the viewer on, the wrapper becomes a button: it takes a tab stop, a name from alt, and opens the viewer on Enter or Space.
  • Your own tabIndex and aria-label win over the defaults.
  • preview={false} removes the button role, the tab stop and the label, leaving the image and its alt. Use it for pictures that are not interactive.
  • Twenty previewable thumbnails are twenty tab stops. If they are not the main content, make the list rows the tab stops and open the viewer with preview.open.
  • Do not put a link or a button inside the wrapper. It is already a button, so the picture would get two tab stops.

What the open viewer is

The viewer is a modal dialog, named from alt.
  • Focus is trapped. Tab cycles through the toolbar and the close button. preview.focusTrap defaults to true, so leave it on, or keyboard users tab through a page they cannot see.
  • Esc closes it, and only the top layer, so Esc in a dialog above the viewer does not close both.
  • Focus goes back to the trigger when it closes, including a button that opened a controlled preview.
  • Page scrolling is locked while it is open, and a click on the backdrop closes it (preview.mask.closable, default true). Esc remains the keyboard route.
  • In a group of two or more, Left and Right move between images.

The toolbar button names

Every toolbar control is a real button with a label, and a disabled one is really disabled. The labels are the problem.
  • The labels are the literal strings flipY, flipX, rotateLeft, rotateRight, zoomOut, zoomIn, prev and next, in English and not localized.
  • Use preview.actionsRender to rename them. It passes you the finished buttons, so clone each one with a real aria-label and it keeps its handler and disabled state. Your renderer replaces the kit's, so swap the icons there too.
  • The next and previous arrows cannot be rendered differently, so their labels stay prev and next.
  • Swapping an icon does not change a button's label.

Loading and broken states

The loading and broken states announce differently, and one of them announces nothing.
  • A placeholder node is hidden from screen readers. Nothing says that the image is on its way.
  • The { progress } form does announce. With a percent it is a progress bar with its value. Without one it is marked busy and reads “Loading”.
  • That “Loading” is English and not localized. Supply your own text through placeholder.progress.render in a localized product.
  • A failed image says nothing. The fallback replaces the picture and alt stays the same. Where it matters that the image is missing, say so from onError in your own live region.

Contrast and motion

Two things need your check: the hover overlay on a thumbnail, and the viewer's animation.
  • The hover cover sits on the picture, so its contrast is against the picture, not the page. White text on a scrim can read on one photo and vanish on the next. Make the scrim dark enough for the brightest image you will show.
  • The cover only appears on hover, which touch and keyboard users do not get. Anything it says must also be in the wrapper's label.
  • preview.mask.blur is off by default. Blur is decoration, so the backdrop still has to separate the viewer on its own.
  • Zoom, rotate and pan only happen when the reader asks. Put the viewer's fade and anything you add behind motion-safe:, or turn transitions off in the theme with token: { motion: false }.
  • preview.wheel defaults to true, so the wheel zooms the open viewer instead of scrolling. Turn it off where nobody needs to magnify the picture.