Accessibility pattern · Media

Complex image

A chart needs two alternatives: a short one that names it and says what it shows, and a long one that holds the detail. Here the short alt is on the chart, aria-describedby ties it to the data table beside it, and a details element holds the description in words for everyone.

WCAG criteria
5
Keyboard rules
2
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.

Mumbai's rain comes in four months Average monthly rainfall at Santacruz, 1991 to 2020. Source: India Meteorological Department.
mm 0 250 500 750 1,000 Jan J Feb F Mar M Apr A May M Jun J Jul J Aug A Sep S Oct O Nov N Dec D 920

Shaded: June to September, when over 95% of the year's rain falls.

Average rainfall by month, in millimetres
MonthRainfall (mm)
January0.2
February0.2
March0.1
April0.1
May7.3
June526.3
July919.9
August560.8
September383.5
October91.3
November11.0
December1.6
Year2,502.3
Describe this image

A bar chart of average rainfall for each month of the year, in millimetres, measured at Santacruz in Mumbai.

From January to April the bars barely leave the axis, under 1 mm a month, and May brings 7 mm. Then the monsoon arrives: 526 mm in June, a peak of 920 mm in July, 561 mm in August and 384 mm in September.

October falls back to 91 mm, November to 11 mm and December to 2 mm. Of the year's 2,502 mm, over 95% falls from June to September, and July alone brings more than a third.

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
TabMoves to Describe this image. The chart is not a tab stop, because nothing in it can be operated.
Enter or SpaceOpens or closes the long description.

Screen readers

What it announces

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

WhenExpected announcement
Reading reaches the figureMumbai's rain comes in four months. Average monthly rainfall at Santacruz, 1991 to 2020. Source: India Meteorological Department., figure
Reading reaches the chartBar chart of Mumbai's average monthly rainfall: over 95% falls from June to September, peaking at 920 mm in July., image. Average rainfall by month, in millimetres. Month, Rainfall (mm), January, 0.2…
The screen reader supports aria-details… image, has details (a key moves to the description)
Focus reaches the disclosureDescribe this image, button, collapsed
Enter opens itExpanded
Reading reaches the tableAverage rainfall by month, in millimetres, table, 2 columns, 14 rows

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-cimg" data-ap-complex-image>
  <figure class="ap-cimg__fig">
    <figcaption class="ap-cimg__cap">
      <strong class="ap-cimg__point">Mumbai's rain comes in four months</strong>
      <span class="ap-cimg__source">Average monthly rainfall at Santacruz, 1991 to 2020. Source: India Meteorological Department.</span>
    </figcaption>

    <div class="ap-cimg__body">
      <div class="ap-cimg__chartbox">
        <svg class="ap-cimg__chart" viewBox="0 0 480 330" role="img" aria-label="Bar chart of Mumbai's average monthly rainfall: over 95% falls from June to September, peaking at 920 mm in July." aria-describedby="cimg-table" aria-details="cimg-long">
          <rect class="ap-cimg__band" x="234.33" y="44" width="134.67" height="246" rx="6"/>
          <text class="ap-cimg__unit" x="58" y="30" text-anchor="end">mm</text>
        <line class="ap-cimg__grid" x1="66" x2="470" y1="290" y2="290"/>
        <text class="ap-cimg__tick" x="58" y="295" text-anchor="end">0</text>
        <line class="ap-cimg__grid" x1="66" x2="470" y1="231.5" y2="231.5"/>
        <text class="ap-cimg__tick" x="58" y="236.5" text-anchor="end">250</text>
        <line class="ap-cimg__grid" x1="66" x2="470" y1="173" y2="173"/>
        <text class="ap-cimg__tick" x="58" y="178" text-anchor="end">500</text>
        <line class="ap-cimg__grid" x1="66" x2="470" y1="114.5" y2="114.5"/>
        <text class="ap-cimg__tick" x="58" y="119.5" text-anchor="end">750</text>
        <line class="ap-cimg__grid" x1="66" x2="470" y1="56" y2="56"/>
        <text class="ap-cimg__tick" x="58" y="61" text-anchor="end">1,000</text>
          <line class="ap-cimg__axis" x1="66" x2="470" y1="290" y2="290"/>
        <g class="ap-cimg__month" data-m="0">
          <rect class="ap-cimg__bar" x="71.83" y="288.5" width="22" height="1.5" rx="3"/>
          <text class="ap-cimg__label" x="82.83" y="316" text-anchor="middle">Jan</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="82.83" y="318" text-anchor="middle">J</text>
        </g>
        <g class="ap-cimg__month" data-m="1">
          <rect class="ap-cimg__bar" x="105.5" y="288.5" width="22" height="1.5" rx="3"/>
          <text class="ap-cimg__label" x="116.5" y="316" text-anchor="middle">Feb</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="116.5" y="318" text-anchor="middle">F</text>
        </g>
        <g class="ap-cimg__month" data-m="2">
          <rect class="ap-cimg__bar" x="139.17" y="288.5" width="22" height="1.5" rx="3"/>
          <text class="ap-cimg__label" x="150.17" y="316" text-anchor="middle">Mar</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="150.17" y="318" text-anchor="middle">M</text>
        </g>
        <g class="ap-cimg__month" data-m="3">
          <rect class="ap-cimg__bar" x="172.83" y="288.5" width="22" height="1.5" rx="3"/>
          <text class="ap-cimg__label" x="183.83" y="316" text-anchor="middle">Apr</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="183.83" y="318" text-anchor="middle">A</text>
        </g>
        <g class="ap-cimg__month" data-m="4">
          <rect class="ap-cimg__bar" x="206.5" y="288.3" width="22" height="1.7" rx="3"/>
          <text class="ap-cimg__label" x="217.5" y="316" text-anchor="middle">May</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="217.5" y="318" text-anchor="middle">M</text>
        </g>
        <g class="ap-cimg__month" data-m="5">
          <rect class="ap-cimg__bar" x="240.17" y="166.8" width="22" height="123.2" rx="3"/>
          <text class="ap-cimg__label" x="251.17" y="316" text-anchor="middle">Jun</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="251.17" y="318" text-anchor="middle">J</text>
        </g>
        <g class="ap-cimg__month" data-m="6">
          <rect class="ap-cimg__bar" x="273.83" y="74.7" width="22" height="215.3" rx="3"/>
          <text class="ap-cimg__label" x="284.83" y="316" text-anchor="middle">Jul</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="284.83" y="318" text-anchor="middle">J</text>
        </g>
        <g class="ap-cimg__month" data-m="7">
          <rect class="ap-cimg__bar" x="307.5" y="158.8" width="22" height="131.2" rx="3"/>
          <text class="ap-cimg__label" x="318.5" y="316" text-anchor="middle">Aug</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="318.5" y="318" text-anchor="middle">A</text>
        </g>
        <g class="ap-cimg__month" data-m="8">
          <rect class="ap-cimg__bar" x="341.17" y="200.3" width="22" height="89.7" rx="3"/>
          <text class="ap-cimg__label" x="352.17" y="316" text-anchor="middle">Sep</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="352.17" y="318" text-anchor="middle">S</text>
        </g>
        <g class="ap-cimg__month" data-m="9">
          <rect class="ap-cimg__bar" x="374.83" y="268.6" width="22" height="21.4" rx="3"/>
          <text class="ap-cimg__label" x="385.83" y="316" text-anchor="middle">Oct</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="385.83" y="318" text-anchor="middle">O</text>
        </g>
        <g class="ap-cimg__month" data-m="10">
          <rect class="ap-cimg__bar" x="408.5" y="287.4" width="22" height="2.6" rx="3"/>
          <text class="ap-cimg__label" x="419.5" y="316" text-anchor="middle">Nov</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="419.5" y="318" text-anchor="middle">N</text>
        </g>
        <g class="ap-cimg__month" data-m="11">
          <rect class="ap-cimg__bar" x="442.17" y="288.5" width="22" height="1.5" rx="3"/>
          <text class="ap-cimg__label" x="453.17" y="316" text-anchor="middle">Dec</text>
          <text class="ap-cimg__label ap-cimg__label--short" x="453.17" y="318" text-anchor="middle">D</text>
        </g>
          <text class="ap-cimg__peak" x="284.83" y="66.7" text-anchor="middle">920</text>
        </svg>
        <p class="ap-cimg__key"><svg class="ap-cimg__swatch" viewBox="0 0 18 18" aria-hidden="true" focusable="false"><rect x=".5" y=".5" width="17" height="17" rx="4.5"/></svg><span><strong>Shaded: June to September,</strong> when over 95% of the year's rain falls.</span></p>
      </div>

      <table class="ap-cimg__table" id="cimg-table">
        <caption>Average rainfall by month, in millimetres</caption>
        <thead>
          <tr><th scope="col">Month</th><th scope="col">Rainfall (mm)</th></tr>
        </thead>
        <tbody>
          <tr data-m="0"><th scope="row">January</th><td>0.2</td></tr>
          <tr data-m="1"><th scope="row">February</th><td>0.2</td></tr>
          <tr data-m="2"><th scope="row">March</th><td>0.1</td></tr>
          <tr data-m="3"><th scope="row">April</th><td>0.1</td></tr>
          <tr data-m="4"><th scope="row">May</th><td>7.3</td></tr>
          <tr data-m="5"><th scope="row">June</th><td>526.3</td></tr>
          <tr data-m="6"><th scope="row">July</th><td>919.9</td></tr>
          <tr data-m="7"><th scope="row">August</th><td>560.8</td></tr>
          <tr data-m="8"><th scope="row">September</th><td>383.5</td></tr>
          <tr data-m="9"><th scope="row">October</th><td>91.3</td></tr>
          <tr data-m="10"><th scope="row">November</th><td>11.0</td></tr>
          <tr data-m="11"><th scope="row">December</th><td>1.6</td></tr>
        </tbody>
        <tfoot>
          <tr><th scope="row">Year</th><td>2,502.3</td></tr>
        </tfoot>
      </table>
    </div>

    <details class="ap-cimg__long" id="cimg-long">
      <summary class="ap-cimg__toggle">
        <svg class="ap-cimg__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 6 6 6-6 6"/></svg>
        Describe this image
      </summary>
      <div class="ap-cimg__words">
        <p>A bar chart of average rainfall for each month of the year, in millimetres, measured at Santacruz in Mumbai.</p>
        <p>From January to April the bars barely leave the axis, under 1 mm a month, and May brings 7 mm. Then the monsoon arrives: 526 mm in June, a peak of 920 mm in July, 561 mm in August and 384 mm in September.</p>
        <p>October falls back to 91 mm, November to 11 mm and December to 2 mm. Of the year's 2,502 mm, over 95% falls from June to September, and July alone brings more than a third.</p>
      </div>
    </details>
  </figure>
</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

    The chart has a short alternative that names it and gives its main point, and a long one in words and as data.

  • 1.3.1 Info and Relationships Level A

    The figure is named by its caption, the table has a caption and header cells, and aria-describedby ties the chart to its data.

  • 1.4.1 Use of Color Level A

    The monsoon months are marked by a shaded band with a written key, not by a change of bar color.

  • 1.4.11 Non-text Contrast Level AA

    Each bar clears 3:1 against the card, and the axis line is drawn in the control edge color.

  • 4.1.2 Name, Role, Value Level A

    Describe this image is a native summary, so it is a button that reports expanded or collapsed.

Usage

When to use it

Use it

  • Charts, graphs, maps, diagrams and infographics whose information cannot fit in one sentence.
  • Any picture that carries data people may want to read exactly, compare or copy.

Use something else

  • A picture whose point fits in a sentence: an alt is enough, as in the image alternatives pattern.
  • A chart people need to explore point by point: make the chart itself interactive, as in the bar chart pattern.
  • Decoration made to look like a chart: hide it, and don't describe it.

Common failures

How it usually goes wrong

  • The whole description in the alt

    A paragraph in alt is read as one unbroken run that cannot be skimmed, and sighted readers never see it. Keep alt short and put the detail on the page.

  • Relying on longdesc

    longdesc is obsolete in HTML and browsers never offered a dependable way to reach it. A visible disclosure or a link next to the image works for everyone.

  • Data only in the picture

    Numbers drawn into a chart cannot be read exactly, copied or translated. Put them in a real table as well.

  • A description only screen readers get

    A visually hidden long description shuts out sighted people who find charts hard to read. Show it, or let anyone open it.

  • The key point carried by color

    Highlighting the monsoon months in a different shade alone fails people who cannot tell the shades apart. Add a band, a label or a pattern.

Notes

Building it

  • Write the short alt as "what it is: what it shows". The chart type helps people picture it; the main point is why it is there.
  • aria-describedby reads its target as one flat run of words straight after the name. That suits a short table like this one; for a long one, point it at a summary sentence instead.
  • aria-details points at structured content and leaves it in place: screen readers that support it say "has details" and let people go there, with headings and table structure intact. Support is still uneven, so use it alongside a visible way in.
  • The longdesc attribute is obsolete in the HTML standard and was never dependably supported; a details element or a link beside the image replaces it.
  • The chart is drawn as inline SVG with role="img", so its labels and shapes are hidden from screen readers, which get the alt and the table instead.

Sources: W3C WAI: Complex images · WAI-ARIA 1.2: aria-details · India Meteorological Department: Climatological normals

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