# Email Accessibility Check (HTML email templates) (`madrasco/email-accessibility-check`) Actor

Batch-checks HTML email templates for machine-checkable accessibility problems: missing alt text, low colour contrast, missing lang and title, layout tables without role=presentation, heading jumps, vague link text, tiny or all-caps text. One row and one report per template.

- **URL**: https://apify.com/madrasco/email-accessibility-check.md
- **Developed by:** [Madrasco](https://apify.com/madrasco) (community)
- **Categories:** Developer tools, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Email Accessibility Check (HTML email templates)

Check a batch of **HTML email templates** for accessibility problems a machine can verify. Give the actor template URLs, paste the HTML, or point it at a key-value store of uploaded `.html` files. For each template you get **one dataset row** (counts by severity and rule, plus every finding) and a readable **HTML and Markdown report** in the key-value store.

Each finding has a **rule ID**, a **severity**, a plain-English message, the element's **CSS selector path**, its **line number** and a **snippet** of the start tag, so you can find it in your template.

### What it checks

| Rule ID | Severity | What it flags | Based on |
|---|---|---|---|
| `img-alt-missing` | error | `<img>` with no `alt` attribute | WCAG 2.2 SC 1.1.1 |
| `img-spacer-alt` | warning | Spacer or tracking image (1-2 px wide/high, or named like `spacer.gif`) whose `alt` is not empty | WCAG 2.2 SC 1.1.1 |
| `img-alt-filename` | warning | Alt text that is a file name (`IMG_0042.jpg`) | WCAG 2.2 SC 1.1.1 |
| `link-no-name` | error | Link with no text and only images with empty or missing alt | WCAG 2.2 SC 2.4.4 |
| `link-text-vague` | warning | Link text such as "click here", "here", "read more", "learn more" | WCAG 2.2 SC 2.4.4 |
| `link-text-url` | warning | Link text that is a bare URL | WCAG 2.2 SC 2.4.4 |
| `contrast-low` | error | Text whose colour and background colour are both declared, with a contrast ratio below 4.5:1 (3:1 for large text: 24px, or 18.66px bold), computed with the WCAG 2.x relative-luminance formula | WCAG 2.2 SC 1.4.3 |
| `html-lang-missing` | error | `<html>` without `lang` (or `xml:lang`) | WCAG 2.2 SC 3.1.1 |
| `title-missing` | warning | No `<title>`, or an empty one | WCAG 2.2 SC 2.4.2 |
| `table-role-missing` | warning | Table without `role="presentation"` (or `none`) that has no `<th>`, `<caption>` or `summary`, so it is treated as a layout table | WCAG 2.2 SC 1.3.1 |
| `heading-order` | warning | Heading level that skips a level (`<h1>` then `<h3>`) | WCAG 2.2 SC 1.3.1 (practice check) |
| `font-size-small` | warning | Text with a declared font size below 14px (configurable) | practice check, not a WCAG rule |
| `all-caps-text` | warning | 20+ letters over 3+ words in capitals, typed or via `text-transform: uppercase` | practice check, not a WCAG rule |
| `preheader-missing` | info | No hidden preheader text before the first visible text | practice check |

Styles are read from inline `style` attributes, `<style>` blocks (type, class, ID and descendant selectors, with specificity and `!important`), and legacy attributes (`bgcolor`, `background`, `<font color size>`, `<body text>`). Hidden text (`display:none`, zero font size, `visibility:hidden`, `max-height:0` with `overflow:hidden`, `mso-hide:all`, `aria-hidden`) is skipped. Font size and visibility are inherited the way a browser inherits them, so text that sets its own size inside a `font-size:0` wrapper (as MJML and hybrid layouts produce) is checked.

**How we tested it:** on 133 public open-source email templates from 8 GitHub repositories (28 of them MJML output), run 2026-09-26, the actor flagged low contrast in the same 99 templates as the open-source axe-core checker and found none in the same 34.

### What it does not check

Each report has a **Not checked** section that counts what the actor could not assess in that template, for example:

- text over **background images** or gradients (contrast can't be measured from HTML);
- text whose colour or background is **not declared** (the email client's default applies; we don't guess);
- colours written as `hsl()`, `var()` or other forms it can't resolve;
- `@media` blocks (dark-mode and mobile styles) and complex selectors (pseudo-classes, attribute selectors);
- external stylesheets (not fetched);
- **text inside images** and whether alt text is accurate (needs a person);
- whether a **plain-text version** is sent (that is set in your email platform, not in the HTML).

**This actor does not certify WCAG conformance or legal compliance** (for example under accessibility laws). It runs a fixed list of automated checks; many accessibility requirements need a person to judge, and email clients change how HTML is rendered.

### Input

- **Template URLs**: links to raw HTML templates that work without a login. Private and local network addresses are refused; each download has a 30-second network timeout and a 5 MB limit by default.
- **HTML templates (pasted)**: a JSON list of `{"name": "...", "html": "..."}` objects, or plain HTML strings.
- **Key-value store with uploaded templates**: pick a store from your account; every record whose key ends in `.html`/`.htm` or whose content type is HTML is checked (or only the **keys** you list).
- **Include notes** (default on), **Minimum font size** (default 14px), **Maximum template size** (default 5 MB).

Up to 500 templates per run.

### Output

**Dataset**: one row per template (view "Templates"; view "Findings" shows one line per finding):

```json
{"name": "newsletter.html", "source": "https://example.com/newsletter.html", "status": "checked",
 "errors": 2, "warnings": 1, "infos": 0,
 "issuesByRule": {"contrast-low": 1, "img-alt-missing": 1, "link-text-vague": 1},
 "findings": [{"rule": "contrast-low", "severity": "error", "line": 19,
               "selector": "html > body > table > tr > td > p:nth-of-type(1)", "snippet": "<p class=\"muted\">",
               "message": "Contrast 2.32:1 (#aaaaaa on #ffffff); needs 4.5:1 for normal text (16px).",
               "text": "Low contrast grey text on white.", "contrastRatio": 2.32, "required": 4.5,
               "foreground": "#aaaaaa", "background": "#ffffff"}],
 "notChecked": [{"what": "text over background images (contrast not measurable)", "count": 1}],
 "reportKey": "001-newsletter.html", "reportUrl": "https://api.apify.com/v2/key-value-stores/.../records/001-newsletter.html"}
```

(Shortened example.) A template that can't be loaded gets a row with status `load-failed` and the reason.

**Key-value store**: `NNN-<name>.html` and `NNN-<name>.md` per template, and `OUTPUT` with run totals.

If your account's storage access is set to Restricted, the `reportUrl` link opens only while you are signed in to Apify; you can also open the reports from the run's Storage tab.

### Privacy

Templates are loaded into the run, checked in memory and written only to your own Apify storage. Nothing is sent anywhere else. Pasted HTML is kept in the run input like any Apify input.

### Support

Open an issue on the actor's **Issues** tab. This actor is built and maintained with AI assistance by Madrasco; a human owner can be reached on request through the Issues tab.

# Actor input Schema

## `urls` (type: `array`):

Links to HTML email templates (the raw .html file, e.g. a hosted 'view in browser' page or a raw file link). Must be reachable without a login. Up to 500 templates per run across all inputs.

## `htmlTemplates` (type: `array`):

Paste templates directly: a list of objects {"name": "welcome", "html": "<html>...</html>"}, or a list of HTML strings.

## `keyValueStoreId` (type: `string`):

Pick a key-value store in your Apify account holding .html records (for example uploaded through the Storage tab). All records whose key ends in .html/.htm or whose content type is HTML are checked, unless you list keys below.

## `keys` (type: `array`):

Optional: record keys to check in the key-value store above.

## `includeInfo` (type: `boolean`):

Also list informational notes (currently: no hidden preheader text found). Errors and warnings are always listed.

## `minFontSizePx` (type: `integer`):

Text with a declared font size below this is flagged (rule font-size-small).

## `maxDownloadMb` (type: `integer`):

Templates larger than this are reported as a load failure.

## Actor input object example

```json
{
  "urls": [
    "https://raw.githubusercontent.com/leemunroe/responsive-html-email-template/master/email.html"
  ],
  "htmlTemplates": [
    {
      "name": "example",
      "html": "<html><head><title>Hi</title></head><body><p style=\"color:#999;background:#fff\">Hello</p><a href=\"https://example.com\">Click here</a><img src=\"logo.png\"></body></html>"
    }
  ],
  "includeInfo": true,
  "minFontSizePx": 14,
  "maxDownloadMb": 5
}
```

# Actor output Schema

## `templates` (type: `string`):

One dataset row per email template with its status and issue counts (the Findings view lists each finding).

## `files` (type: `string`):

A Markdown and HTML report for each template.

## `summary` (type: `string`):

Totals across all templates.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://raw.githubusercontent.com/leemunroe/responsive-html-email-template/master/email.html"
    ],
    "htmlTemplates": [
        {
            "name": "example",
            "html": "<html><head><title>Hi</title></head><body><p style=\"color:#999;background:#fff\">Hello</p><a href=\"https://example.com\">Click here</a><img src=\"logo.png\"></body></html>"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("madrasco/email-accessibility-check").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "urls": ["https://raw.githubusercontent.com/leemunroe/responsive-html-email-template/master/email.html"],
    "htmlTemplates": [{
            "name": "example",
            "html": "<html><head><title>Hi</title></head><body><p style=\"color:#999;background:#fff\">Hello</p><a href=\"https://example.com\">Click here</a><img src=\"logo.png\"></body></html>",
        }],
}

# Run the Actor and wait for it to finish
run = client.actor("madrasco/email-accessibility-check").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://raw.githubusercontent.com/leemunroe/responsive-html-email-template/master/email.html"
  ],
  "htmlTemplates": [
    {
      "name": "example",
      "html": "<html><head><title>Hi</title></head><body><p style=\\"color:#999;background:#fff\\">Hello</p><a href=\\"https://example.com\\">Click here</a><img src=\\"logo.png\\"></body></html>"
    }
  ]
}' |
apify call madrasco/email-accessibility-check --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,madrasco/email-accessibility-check"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/UKtAaOerNHYPOwFAQ/builds/TfF0fz4dfSuhs14Q0/openapi.json
