HTML email accessibility: what to code, and what each client keeps

By Mara Levin · Updated October 7, 2026 · Standards and caniemail data read that day

The short answer

An accessible HTML email declares its language, marks every layout table with role="presentation", uses real headings in order, gives each image an alt that matches its job, writes link text that makes sense alone, and keeps text at 4.5:1 contrast or better in every colour mode. Those habits come from WCAG 2.2 and survive email clients because they are plain HTML. ARIA is the part that does not survive: aria-labelledby and aria-describedby break in Gmail, so build the structure from text, not from ARIA relationships.

What the clients keep: the accessibility attributes on caniemail

Feature Gmail Apple Mail Outlook Windows Yahoo What it means
lang attribute Yes Yes Yes Partial: not on td Put it on html and body; a td carrying it is ignored in the clients marked partial.
role attribute Yes Yes Yes (code inspected, not live-tested) Partial: table only role="presentation" belongs on tables, which is where the partial clients keep it.
aria-label Yes Yes Yes No Safe as an extra, never as the only label.
aria-hidden Yes Yes Yes No Hides duplicates from screen readers where kept; Yahoo and AOL drop it.
aria-labelledby No (buggy) Yes No (id removed) No Gmail prefixes the id but not the reference, so the link breaks.
aria-describedby No (buggy) Yes No No Same id-prefix problem in Gmail webmail.
h1 to h6 Yes Yes Yes Yes The one structural element you can rely on everywhere.
HTML5 semantics (main, nav, section, article) Partial Yes No No Gmail does not support main or nav and swaps their tags for u; Outlook for Windows and Yahoo strip them.

Source: caniemail.com data last updated September 16, 2026. Each row reads the newest entry per client from the linked feature page; "Outlook Windows" is the desktop Word-engine line (2019 is the newest in the data), and Gmail covers webmail and the apps. caniemail has no row for the VoiceOver, TalkBack or NVDA experience itself, only for whether the markup survives.

A skeleton that passes the basics

Before the details, here is the smallest email that satisfies the language, table, heading and reading-order points in one piece. The hidden preheader sits first, the layout tables carry role="presentation", and the heading is a real h1 styled inline because email clients ignore the browser's default heading look only some of the time.

<!DOCTYPE html>
<html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Your invoice for September is ready</title>
</head>
<body lang="en" dir="ltr" style="margin:0; padding:0;">
  <div style="display:none; max-height:0; overflow:hidden;">Invoice 4812 is attached and due on October 21.</div>
  <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
    <tr>
      <td align="center">
        <table role="presentation" width="600" style="max-width:600px;" cellspacing="0" cellpadding="0" border="0">
          <tr>
            <td style="padding:24px; font-family:Arial, Helvetica, sans-serif; font-size:16px; line-height:24px; color:#333333;">
              <h1 style="margin:0 0 16px 0; font-size:24px; line-height:30px;">Your September invoice</h1>
              <p style="margin:0 0 16px 0;">Invoice 4812 is ready. The amount of $240.00 is due on October 21.</p>
            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>
</body>
</html>

Each choice has a source. W3C's guidance on declaring language in HTML says to put the language on the html element rather than body, because body does not cover the head. Email adds a wrinkle: some clients strip the html and body wrappers when they embed the message in their own page, which is why React Email's documentation tells you to repeat the same lang and dir on the body component. Doing it on both costs two attributes.

Declare the language, and mark the exceptions

WCAG 3.1.1 (Language of Page, Level A) exists so that screen readers can load the correct pronunciation rules; the Understanding document says visual browsers also use it to display characters correctly. Criterion 3.1.2 (Language of Parts, Level AA) covers passages in another language:

<p>Your order ships with a <span lang="fr">carte cadeau</span> for the winter sale.</p>

The caniemail entry for lang shows support in Gmail, Apple Mail and Outlook, and a partial result in Yahoo and AOL, where the note says it is not supported on td elements. The practical rule is to attach lang to html, body, p or span, never to the cell that carries your layout. For right-to-left languages, the dir entry lists support in Gmail and Apple Mail, a partial result in Outlook for Windows (buggy on anchor tags), and a Gmail mobile-webmail bug where the client applies rtl to the entire email if any text in it is right-to-left.

Layout tables versus data tables

Email layouts are tables because layout CSS such as flexbox and grid is unavailable in Outlook for Windows, and caniemail's table entry shows the element supported in every client it tracks. The cost is that a screen reader may announce rows and columns for a structure that has none. The fix is role="presentation", which the WAI-ARIA specification describes as an element whose implicit native role semantics will not be mapped to the accessibility API. The role entry on caniemail has two caveats worth reading: the Outlook for Windows result comes from inspecting processed code on Litmus rather than a live test, and Yahoo and AOL keep the attribute only on table.

When the table is the content, do the opposite. W3C's Tables tutorial says header cells must be marked up with th and data cells with td, with scope to define the direction of each header when it is not obvious, and a caption to name the table's topic. An invoice line-item grid, a usage report or a plan comparison belongs in that form:

<table width="100%" cellspacing="0" cellpadding="8" border="0" style="border-collapse:collapse;">
  <caption style="text-align:left; padding:0 0 8px 0;">Usage in September</caption>
  <tr>
    <th scope="col" align="left">Plan</th>
    <th scope="col" align="right">Seats</th>
    <th scope="col" align="right">Amount</th>
  </tr>
  <tr>
    <th scope="row" align="left">Team</th>
    <td align="right">6</td>
    <td align="right">$240.00</td>
  </tr>
</table>

Keep data tables small. A wide grid has to survive a 320 pixel viewport (WCAG 1.4.10 Reflow asks for content without two-dimensional scrolling at that width, except where two-dimensional layout is needed for meaning), and if it cannot, link to a web page that carries the table.

Headings, lists and the elements that do not make it

W3C's headings tutorial says to nest headings by rank and warns that skipping ranks, an h2 followed directly by an h4, can be confusing. The caniemail entry for h1 to h6 shows support across every client it lists, so a heading is the one piece of structure with no caveats. Style it inline (size, weight, margin) and use the rank for meaning, not for size.

HTML5 sectioning is a different story. The semantics entry reports Outlook for Windows as unsupported, Yahoo as unsupported with the tags stripped, and Gmail as partial: main, nav, details and summary are not supported and their tags are replaced by u. Landmarks therefore cannot be your navigation aid in email, and the headings have to do that work. Lists are fine with one caveat: the lists entry marks reversed on ol as unsupported in Gmail and Outlook, which only matters if you count backwards.

Reading order is source order

WCAG 1.3.2 (Meaningful Sequence) says that when the order of content affects meaning, a correct reading sequence must be programmatically determinable. In a table layout the programmatic order is the order of cells in the markup, not the order your eye follows. A two-column product row therefore reads the whole left cell before the right one, and a layout that reverses columns on mobile with dir="rtl" tricks or display tricks reads in source order whatever the screen shows. Write the source in the order a person should hear it: headline, summary, action, then supporting detail, and let the columns stack in that same sequence on small screens. Info and Relationships (1.3.1) is the companion rule: if bold text is acting as a heading or a row of dashes is acting as a list, mark it up as one.

Images: four jobs, four alts

WCAG 1.1.1 (Non-text Content) requires a text alternative that serves the equivalent purpose, and says purely decorative content should be implemented so assistive technology can ignore it. W3C's image tutorials turn that into four jobs, and an email has all of them:

<!-- Informative: say what the picture adds -->
<img src="https://example.com/img/chart-sept.png" width="560" alt="Bar chart: usage grew from 410 to 520 seats between August and September" style="display:block; width:100%; max-width:560px; height:auto;">

<!-- Decorative: empty alt, never omitted -->
<img src="https://example.com/img/divider.png" width="560" height="4" alt="" style="display:block;">

<!-- Functional: describe the action, not the picture -->
<a href="https://example.com/invoices/4812">
  <img src="https://example.com/img/download-pdf.png" width="180" alt="Download invoice 4812 as PDF" style="display:block;">
</a>

<!-- Logo inside a link that already has text: empty alt avoids saying it twice -->
<a href="https://example.com/">
  <img src="https://example.com/img/logo.png" width="120" alt="" style="vertical-align:middle;">
  Acme billing home
</a>

The decorative images page says to provide a null alt, and adds that leaving the attribute out is not an option, because some screen readers then announce the file name. The functional images page says the alternative should convey the action rather than describe the picture, so "print this page" rather than "printer". There is no character limit in either document. Images are also the part of an email most likely to be blocked, which gives alt text a second job: with images off, the alt is what remains. Style it with a font size and colour on the img so a blocked image shows a legible line of text, and never place the only copy of a date, code or price inside an image. Images of Text (1.4.5) says to use text rather than images of text where the technology allows, with logotypes as the exception.

Links and buttons

WCAG 2.4.4 (Link Purpose in Context) says the purpose of each link can be determined from the link text alone or with its programmatic context, except where the purpose would be ambiguous to everyone. A newsletter with five "Read more" links fails in a screen reader's links list; five links that name the article pass. In email, a button is an anchor styled as a button, and React Email's documentation says as much: quite often in the email world people say buttons when they mean links.

<table role="presentation" cellspacing="0" cellpadding="0" border="0">
  <tr>
    <td bgcolor="#0a58ca" style="background-color:#0a58ca; border-radius:4px;">
      <a href="https://example.com/invoices/4812"
         style="display:inline-block; padding:14px 28px; min-height:24px; font-family:Arial, Helvetica, sans-serif; font-size:16px; line-height:20px; font-weight:bold; color:#ffffff; text-decoration:none;">
        Pay invoice 4812
      </a>
    </td>
  </tr>
</table>

Two details in that sample come from standards. The padding makes the target comfortably larger than the 24 by 24 CSS pixel minimum in WCAG 2.2's new Target Size (Minimum) criterion (2.5.8, Level AA); a text link inside a sentence is exempt through the inline exception, but a row of small icon links is not. And the colour pair is chosen for contrast: white on #0a58ca is 6.44:1, whereas white on the #007bff blue many templates use is 3.98:1, below the 4.5:1 that normal-size text needs.

Contrast, size and spacing

WCAG 1.4.3 (Contrast (Minimum), Level AA) asks for at least 4.5:1 for text, and 3:1 for large-scale text. The Understanding page defines large as 18 point, or 14 point bold, and converts them: about 24 px and about 18.5 px. A lot of email advice says 18 px, which is wrong by a wide margin. Logotypes and pure decoration have no requirement. Some pairs worth knowing, computed with the WCAG relative-luminance formula:

Text on background Ratio Normal text
#333333 on white12.63:1Passes
#767676 on white4.54:1Passes, barely; the lightest grey that does
#999999 on white2.85:1Fails, including as large text
white on #f45e433.22:1Fails as body text; passes as large text
white on #0a58ca6.44:1Passes

The orange row is not hypothetical: it is the button colour in the example on the MJML documentation page for mj-button, a reminder to check sample code before you paste it.

Controls and meaningful graphics have their own floor: Non-text Contrast (1.4.11) asks for 3:1 against adjacent colours for user interface components and graphical objects needed to understand content, which is why a pale-grey outline button on white fails even when its label passes. Use of Color (1.4.1) says colour must not be the only visual means of conveying information, so a red price needs the word "overdue" or an icon with text beside it.

Size and spacing sit under Text Spacing (1.4.12), which requires that nothing is lost when a reader sets line height to at least 1.5 times the font size, paragraph spacing to 2 times, letter spacing to 0.12 times and word spacing to 0.16 times. In email, that means avoiding fixed-height cells and tight line-height on text blocks that carry content. Use px for font-size: the font-size entry shows rem unsupported in Outlook for Windows, Yahoo, AOL and the GMX family.

Focus, motion and what to skip

Keyboard users depend on a visible focus indicator, and email is a poor place to restyle it. The :focus entry shows no support in Gmail and Outlook for Windows, and the outline entry shows Gmail supporting it while Outlook for Windows does not. The sensible position is to leave the client's default focus ring alone, which is the opposite of the advice to add outline to every link. For motion, prefers-reduced-motion works in Apple Mail and Outlook.com but not in Gmail, so animated GIFs and CSS animation need a safe still frame as the first frame, and nothing should flash.

What the authoring tools do for you, per their own documentation

Accessibility claims in tool marketing are easy to find and hard to verify, so this section lists only what the documentation I read states. MJML takes lang and dir on the root mjml tag and writes them to the html tag and a wrapping div inside the body; the docs add that MJML puts role="article", aria-roledescription="email" and an aria-label taken from mj-title on that div. Its mj-table accepts role with none or presentation, and mj-image has an alt attribute that defaults to an empty string, so a forgotten alt silently becomes a decorative image. React Email's Html component takes lang (default en) and dir (default ltr), its Img component lists an alt prop, and its render utility can produce a plain-text version, which the docs call important because some recipients disable HTML email for security or accessibility reasons. The same source in both frameworks looks like this:

<mjml lang="en" dir="ltr">
  <mj-head>
    <mj-title>Your September invoice</mj-title>
    <mj-preview>Invoice 4812 is due on October 21.</mj-preview>
  </mj-head>
  <mj-body>
    <mj-section>
      <mj-column>
        <mj-image src="https://example.com/img/logo.png" width="120px" alt="Acme billing" />
        <mj-text font-size="16px" color="#333333">Invoice 4812 is ready.</mj-text>
        <mj-button href="https://example.com/invoices/4812" background-color="#0a58ca" color="#ffffff">
          Pay invoice 4812
        </mj-button>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>
import { Html, Body, Container, Heading, Text, Button, Img, Preview } from "react-email";

export default function Invoice() {
  return (
    <Html lang="en" dir="ltr">
      <Preview>Invoice 4812 is due on October 21.</Preview>
      <Body lang="en" dir="ltr">
        <Container>
          <Img src="https://example.com/img/logo.png" alt="Acme billing" width="120" />
          <Heading as="h1">Your September invoice</Heading>
          <Text>Invoice 4812 is ready.</Text>
          <Button href="https://example.com/invoices/4812">Pay invoice 4812</Button>
        </Container>
      </Body>
    </Html>
  );
}

Both samples follow the documentation pages for MJML and for React Email's Html, Image and render. Neither framework checks your contrast or your link text, and neither can know that an alt is wrong. If you are choosing between them for other reasons, MJML vs React Email covers the rest. Visual builders and testing platforms each publish their own accessibility claims; read them on the vendor's pages and export one real template to inspect the output, as the HTML email builder guide suggests, rather than trusting a feature list.

A release check that fits in ten minutes

Run these against the compiled HTML, not the builder canvas, and write down the client and assistive technology for any exception.

Check How Standard
LanguageSearch the compiled source for lang= on html and bodyWCAG 3.1.1, 3.1.2
Layout tablesEvery layout table has role="presentation"; data tables have th and scopeWCAG 1.3.1
HeadingsList them in source order; no skipped ranksWCAG 1.3.1, 2.4.6
ImagesEvery img has an alt attribute; empty only for decorationWCAG 1.1.1
LinksRead the links list; no two different destinations share a labelWCAG 2.4.4
ContrastCompute each text and button pair, light and dark paletteWCAG 1.4.3, 1.4.11
Images offDisable images; offer, date and call to action are still readableWCAG 1.1.1, 1.4.5
Narrow viewResize to 320 px and to 200% text; nothing clippedWCAG 1.4.4, 1.4.10

For the CSS side of these checks, see the CSS in email guide and the client compatibility guide; for the colour-mode side, the dark mode guide.

Frequently asked questions

Do I need role="presentation" on every table?

On every table that exists for layout, yes. The attribute tells assistive technology to skip the table semantics, which WAI-ARIA defines as an element whose implicit native role will not be mapped to the accessibility API. caniemail lists the role attribute as supported in Gmail, Apple Mail and Outlook for Windows, and as partial in Yahoo and AOL, where it only works on the table tag, so put it on the table and not on td or tr. A real data table, such as the usage grid above, should keep its semantics.

Is alt text under 125 characters a rule?

No. WCAG 1.1.1 asks that non-text content has a text alternative serving the equivalent purpose, and W3C's image tutorials judge alt text by function: informative, decorative, functional or complex. Length limits are folklore. A chart that carries a trend needs a sentence stating the trend; a divider needs alt="". Write what the reader would miss without the image.

Can I rely on ARIA landmarks and aria-labelledby in email?

Not safely. caniemail shows aria-labelledby and aria-describedby as unsupported and buggy in Gmail, because the id gets a prefix that the reference does not, and unsupported in Outlook for Windows. The main and nav elements are dropped by Gmail too. Structure with headings and real text first, and treat aria-label and aria-hidden as optional extras that Gmail, Apple Mail and Outlook for Windows keep but Yahoo and AOL do not.

Does dark mode count as an accessibility feature?

It is a preference you have to survive rather than a feature you can ship. Contrast requirements apply in both modes, and the clients that invert colours on their own can turn a passing pair into a failing one. The dark-mode guide on this site covers which clients allow overrides; for the contrast side, check each text and background pair twice, once in the light palette and once in the dark one.

Which tool should I use to check an email before sending?

Use two layers. First, inspect the compiled HTML for lang, role="presentation", heading order, alt attributes and link text, which a script can flag. Second, listen to the rendered message in a screen reader on the platforms your list uses, because reading order and the way a client rewrites your markup only show up there. Testing platforms advertise accessibility checkers, but read each vendor's own documentation for what it actually reports before you budget for it.