Validation

Lint

@hyper-elements/lint checks an email or a page for what makes it hard to read: links that don't say where they go, images without alternate text, faint or small text, headings out of order, merge syntax typed by hand.

@hyper-elements/lint checks an email or a page for what makes it hard to read or use: links that don’t say where they go, images without alternate text, text too faint or too small, headings out of order, merge syntax typed into the copy by hand. Elements’ own checks say whether a template is correct, and the email client emulator what each email client does to it; lint says whether it’s good.

npm i -D @hyper-elements/lint

It’s a plugin, so it runs wherever validate() and render() do: in a test, on a server, in the browser. For HTML made any other way, there’s lintHtml().

import { render, validate } from "@hyper-elements/react";
import { lint } from "@hyper-elements/lint";

const { html, diagnostics } = await render(<Welcome />, { plugins: [lint()] });
diagnostics[0];
// { code: "email/lint-link-text", severity: "warning", path: "root.children[2]",
//   message: 'The Button says "Click here": a screen reader lists links by their words alone',
//   fix: 'say where it goes ("Confirm your email"), or give it an aria-label',
//   see: "https://www.hyperelements.dev/docs/lint#link-text" }

Its findings are diagnostics like Elements’ own: warnings and notes, never errors, so lint never stops a render. On an Email they’re email/lint-<rule>, on a Page page/lint-<rule>; a Document and a Canvas get none. Each says where in the template it is, what to do, and links to its rule below.

It reads the template, not the HTML

Lint reads the laid-out template, as each output writes it, so it knows what a reader gets:

  • At desktop and at phone width. A block hidden on phones isn’t checked there, and text gets the size it’s shown at on each: 16px on desktop and 12px on phones is small on phones. A finding names the width it’s found at where it’s only one.
  • What’s behind the text. Contrast is checked against the nearest background, with see-through ones mixed in, as WCAG does. Text over a Layer’s image isn’t checked where the image is drawn.
  • What each link says. A Button’s words or aria-label, a Link’s words, or the alt of the images it holds.

Options

lint({
  rules: { "text-size": "off", contrast: "info" }, // a rule's severity, or off
  kind: "marketing",                               // a marketing email needs a way to unsubscribe
});

rules sets a rule’s severity, "off", "info" or "warning", by its id. kind is "marketing" or "transactional": "marketing" turns on unsubscribe. A rule, severity or kind lint doesn’t know throws a TypeError when lint() is called.

rules lists every rule with its default severity, the roots it runs on, what it checks, its fix and its docs link, for a tool that shows them:

import { rules } from "@hyper-elements/lint";
rules.find((rule) => rule.id === "contrast");
// { id: "contrast", severity: "warning", roots: ["Email", "Page"], summary: "…", fix: "…", see: "…#contrast" }

From the command line

@hyper-elements/cli runs lint in check, render and dev, with its options from package.json:

{ "hyperElements": { "lint": { "rules": { "text-size": "off" }, "kind": "marketing" } } }
npx hyper-elements check emails/                   # lint's findings with the other checks
npx hyper-elements check emails/ --max-warnings 0  # fail CI on any warning
npx hyper-elements check email.html                # lintHtml() on any HTML email

Any HTML email

lintHtml(), from @hyper-elements/lint/html, runs the rules HTML can show on any HTML email, however it was made (MJML, React Email, by hand). It needs nothing else installed: no Elements, no React.

import { lintHtml } from "@hyper-elements/lint/html";

const { diagnostics } = lintHtml(html, { rules: { title: "off" } });
diagnostics[0];
// { code: "lint/image-alt", severity: "warning", line: 42, column: 7,
//   message: 'This image has no alt attribute: a screen reader reads out its file name instead (src "hero.png")',
//   fix: "…", see: "https://www.hyperelements.dev/docs/lint#image-alt" }

Its findings are lint/<rule>, each with its line and column. It takes the same options as lint(). On HTML with an <html> or a <body>, a whole document, it also checks the heading order, one <h1>, the language and the title. Outlook’s conditional comments (<!--[if mso]>) are skipped: what’s in them is Outlook’s copy of what’s outside them. An Html block’s markup in a template gets the same rules, at the block’s html, with its line.

HTML can’t say what a template can. lintHtml() doesn’t check contrast or text size (that needs the CSS cascade), and doesn’t check merge syntax: in HTML, a hand-typed {{name}} and a MergeTag’s output look the same.

Rules

image-alt

A link holds only images with no alternate text, so a screen reader has nothing to read out for it. In HTML, any link with nothing to name it, and an image with no alt attribute at all.

A warning by default, on an Email and a Page, and on HTML (an Html block’s and lintHtml()’s). Fix: give the image an alt that says where the link goes, or the link an aria-label.

alt-text

An image’s alternate text is a file name, a URL or a word like “image” or “logo”, which says nothing about it.

A note by default, on an Email and a Page. Fix: say what the image shows (“Tarn’s logo”, “A blue bowl”), or leave alt empty if it’s only decoration.

A link or button says only “click here”, “read more” or the like: screen readers list links by their words alone.

A warning by default, on an Email and a Page, and on HTML (an Html block’s and lintHtml()’s). Fix: say where it goes (“Confirm your email”), or give it an aria-label.

A link or button has no words, alternate text, aria-label or title: nothing names it.

A warning by default, on an Email and a Page. Fix: give it words, or an aria-label.

A link goes nowhere (empty, #, a mailto: or tel: with no address or number), over http: rather than https:, or (in HTML) runs a script.

A warning by default, on an Email and a Page, and on HTML (an Html block’s and lintHtml()’s). Fix: link to the page it’s for, over https:.

heading-order

A heading is more than one level below the heading before it (an h2 followed by an h4).

A warning by default, on an Email and a Page, and on a whole HTML document (lintHtml()’s). Fix: use the next level down, and style it as you want it to look.

heading-one

There’s more than one level-1 heading.

A note by default, on an Email and a Page, and on a whole HTML document (lintHtml()’s). Fix: keep one level-1 heading, the title, and use level 2 for the rest.

heading-empty

A heading has no words.

A warning by default, on an Email and a Page, and on HTML (an Html block’s and lintHtml()’s). Fix: give it words, or take it out.

contrast

Text is too faint against its background to read easily: under WCAG AA’s 4.5:1, or 3:1 for large text.

A warning by default, on an Email and a Page. Fix: darken the text or lighten the background (or the reverse) until it reaches the ratio.

text-size

Body text is under 14px, hard to read on a phone.

A note by default, on an Email and a Page. Fix: make it 14px or larger, on phones at least (style.mobile.fontSize).

lang

The email or page doesn’t say its language, so a screen reader may read it in the wrong one.

A warning by default, on an Email and a Page, and on a whole HTML document (lintHtml()’s). Fix: give the root a lang (“en”, “fr”) (in HTML, give a lang (“en”, “fr”)).

dir

The language is written right to left, but the root doesn’t say so.

A warning by default, on an Email and a Page. Fix: give the root dir=“rtl”.

title

The page has no title for the browser’s tab and for screen readers.

A note by default, on a Page, and on a whole HTML document (lintHtml()’s). Fix: give the root a title (in HTML, give the email a in its <head>).</p> <h3 id="preview-text"><code>preview-text</code></h3> <p>The email has no preview text, or a very short one: inboxes show its first words instead.</p> <p>A note by default, on a Email. <strong>Fix:</strong> give the Email a previewText of 30 characters or more.</p> <h3 id="merge-syntax"><code>merge-syntax</code></h3> <p>Merge syntax ({{name}}, <em>|NAME|</em>, %%name%%) is typed into the text, where a MergeTag would write it.</p> <p>A warning by default, on an Email and a Page. <strong>Fix:</strong> use <MergeTag name="…" />, which writes your sending service’s syntax (the mergeTag option).</p> <h3 id="unsubscribe"><code>unsubscribe</code></h3> <p>A marketing email (kind: “marketing”) has no unsubscribe link.</p> <p>A warning by default, on a Email. <strong>Fix:</strong> add a link to unsubscribe or manage preferences, or the service’s MergeTag for one.</p> <h3 id="video-title"><code>video-title</code></h3> <p>A video’s title is the default, “Video”: the alternate text of its thumbnail in an email.</p> <p>A note by default, on an Email and a Page. <strong>Fix:</strong> give it a title that says what the video is.</p> <h3 id="role-button"><code>role-button</code></h3> <p>A link has role=“button”: screen readers announce a button, which a link in an email isn’t.</p> <p>A warning by default, on HTML (an <code>Html</code> block’s and <code>lintHtml()</code>’s). <strong>Fix:</strong> take the role off: a link is a link, however it looks.</p> <!-- rules:end --> </div><nav class="pager" aria-label="Previous and next" data-astro-cid-lpetmyok><a href="/docs/email-client-emulator" data-astro-cid-lpetmyok><span data-astro-cid-lpetmyok>Previous</span>Email client emulator</a><a class="next" href="/docs/guides/sending" data-astro-cid-lpetmyok><span data-astro-cid-lpetmyok>Next</span>Sending email</a></nav></article><aside class="docs-toc" aria-label="On this page" data-astro-cid-lpetmyok><p class="side-title" data-astro-cid-lpetmyok>On this page</p><ul data-astro-cid-lpetmyok><li class="d2" data-astro-cid-lpetmyok><a href="#it-reads-the-template-not-the-html" data-astro-cid-lpetmyok>It reads the template, not the HTML</a></li><li class="d2" data-astro-cid-lpetmyok><a href="#options" data-astro-cid-lpetmyok>Options</a></li><li class="d2" data-astro-cid-lpetmyok><a href="#from-the-command-line" data-astro-cid-lpetmyok>From the command line</a></li><li class="d2" data-astro-cid-lpetmyok><a href="#any-html-email" data-astro-cid-lpetmyok>Any HTML email</a></li><li class="d2" data-astro-cid-lpetmyok><a href="#rules" data-astro-cid-lpetmyok>Rules</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#image-alt" data-astro-cid-lpetmyok>image-alt</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#alt-text" data-astro-cid-lpetmyok>alt-text</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#link-text" data-astro-cid-lpetmyok>link-text</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#link-empty" data-astro-cid-lpetmyok>link-empty</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#link-target" data-astro-cid-lpetmyok>link-target</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#heading-order" data-astro-cid-lpetmyok>heading-order</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#heading-one" data-astro-cid-lpetmyok>heading-one</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#heading-empty" data-astro-cid-lpetmyok>heading-empty</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#contrast" data-astro-cid-lpetmyok>contrast</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#text-size" data-astro-cid-lpetmyok>text-size</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#lang" data-astro-cid-lpetmyok>lang</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#dir" data-astro-cid-lpetmyok>dir</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#title" data-astro-cid-lpetmyok>title</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#preview-text" data-astro-cid-lpetmyok>preview-text</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#merge-syntax" data-astro-cid-lpetmyok>merge-syntax</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#unsubscribe" data-astro-cid-lpetmyok>unsubscribe</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#video-title" data-astro-cid-lpetmyok>video-title</a></li><li class="d3" data-astro-cid-lpetmyok><a href="#role-button" data-astro-cid-lpetmyok>role-button</a></li></ul><div class="toc-extra" data-astro-cid-lpetmyok><a href="/llms.txt" data-astro-cid-lpetmyok>llms.txt</a><a href="https://github.com/hyper-elements/hyper-elements" data-astro-cid-lpetmyok>Edit on GitHub</a></div></aside></div><dialog class="lightbox" aria-label="Image at full size" data-astro-cid-lpetmyok><div class="lightbox-bar" data-astro-cid-lpetmyok><p class="lightbox-caption" data-astro-cid-lpetmyok></p><div class="lightbox-actions" data-astro-cid-lpetmyok><button type="button" data-step="-1" aria-label="Previous image" data-astro-cid-lpetmyok>←</button><button type="button" data-step="1" aria-label="Next image" data-astro-cid-lpetmyok>→</button><a class="lightbox-open" target="_blank" rel="noopener" data-astro-cid-lpetmyok>Open in a new tab</a><button type="button" data-close aria-label="Close" autofocus data-astro-cid-lpetmyok>✕</button></div></div><div class="lightbox-body" data-astro-cid-lpetmyok><img alt="" data-astro-cid-lpetmyok></div></dialog><script type="text/markdown" id="page-markdown"># Lint @hyper-elements/lint checks an email or a page for what makes it hard to read: links that don't say where they go, images without alternate text, faint or small text, headings out of order, merge syntax typed by hand. `@hyper-elements/lint` checks an email or a page for what makes it hard to read or use: links that don't say where they go, images without alternate text, text too faint or too small, headings out of order, merge syntax typed into the copy by hand. Elements' own checks say whether a template is correct, and the [email client emulator](https://www.hyperelements.dev/docs/email-client-emulator) what each email client does to it; lint says whether it's good. ```bash npm i -D @hyper-elements/lint ``` It's a plugin, so it runs wherever `validate()` and `render()` do: in a test, on a server, in the browser. For HTML made any other way, there's [`lintHtml()`](#any-html-email). ```tsx import { render, validate } from "@hyper-elements/react"; import { lint } from "@hyper-elements/lint"; const { html, diagnostics } = await render(<Welcome />, { plugins: [lint()] }); diagnostics[0]; // { code: "email/lint-link-text", severity: "warning", path: "root.children[2]", // message: 'The Button says "Click here": a screen reader lists links by their words alone', // fix: 'say where it goes ("Confirm your email"), or give it an aria-label', // see: "https://www.hyperelements.dev/docs/lint#link-text" } ``` Its findings are [diagnostics](https://www.hyperelements.dev/docs/diagnostics) like Elements' own: warnings and notes, never errors, so lint never stops a render. On an Email they're `email/lint-<rule>`, on a Page `page/lint-<rule>`; a Document and a Canvas get none. Each says where in the template it is, what to do, and links to its rule below. ## It reads the template, not the HTML Lint reads the laid-out template, as each output writes it, so it knows what a reader gets: - **At desktop and at phone width.** A block hidden on phones isn't checked there, and text gets the size it's shown at on each: 16px on desktop and 12px on phones is small on phones. A finding names the width it's found at where it's only one. - **What's behind the text.** Contrast is checked against the nearest background, with see-through ones mixed in, as WCAG does. Text over a `Layer`'s image isn't checked where the image is drawn. - **What each link says.** A `Button`'s words or `aria-label`, a `Link`'s words, or the `alt` of the images it holds. ## Options ```tsx lint({ rules: { "text-size": "off", contrast: "info" }, // a rule's severity, or off kind: "marketing", // a marketing email needs a way to unsubscribe }); ``` `rules` sets a rule's severity, `"off"`, `"info"` or `"warning"`, by its id. `kind` is `"marketing"` or `"transactional"`: `"marketing"` turns on [`unsubscribe`](#unsubscribe). A rule, severity or kind lint doesn't know throws a `TypeError` when `lint()` is called. `rules` lists every rule with its default severity, the roots it runs on, what it checks, its fix and its docs link, for a tool that shows them: ```ts import { rules } from "@hyper-elements/lint"; rules.find((rule) => rule.id === "contrast"); // { id: "contrast", severity: "warning", roots: ["Email", "Page"], summary: "…", fix: "…", see: "…#contrast" } ``` ## From the command line [`@hyper-elements/cli`](https://www.hyperelements.dev/docs/cli#lint) runs lint in `check`, `render` and `dev`, with its options from `package.json`: ```json { "hyperElements": { "lint": { "rules": { "text-size": "off" }, "kind": "marketing" } } } ``` ```bash npx hyper-elements check emails/ # lint's findings with the other checks npx hyper-elements check emails/ --max-warnings 0 # fail CI on any warning npx hyper-elements check email.html # lintHtml() on any HTML email ``` ## Any HTML email `lintHtml()`, from `@hyper-elements/lint/html`, runs the rules HTML can show on any HTML email, however it was made (MJML, React Email, by hand). It needs nothing else installed: no Elements, no React. ```ts import { lintHtml } from "@hyper-elements/lint/html"; const { diagnostics } = lintHtml(html, { rules: { title: "off" } }); diagnostics[0]; // { code: "lint/image-alt", severity: "warning", line: 42, column: 7, // message: 'This image has no alt attribute: a screen reader reads out its file name instead (src "hero.png")', // fix: "…", see: "https://www.hyperelements.dev/docs/lint#image-alt" } ``` Its findings are `lint/<rule>`, each with its line and column. It takes the same options as `lint()`. On HTML with an `<html>` or a `<body>`, a whole document, it also checks the heading order, one `<h1>`, the language and the title. Outlook's conditional comments (`<!--[if mso]>`) are skipped: what's in them is Outlook's copy of what's outside them. An `Html` block's markup in a template gets the same rules, at the block's `html`, with its line. HTML can't say what a template can. `lintHtml()` doesn't check contrast or text size (that needs the CSS cascade), and doesn't check merge syntax: in HTML, a hand-typed `{{name}}` and a `MergeTag`'s output look the same. ## Rules <!-- rules:start (written from the package's rules by its docs test: UPDATE_LINT_DOCS=1 pnpm --filter @hyper-elements/lint test docs) --> ### `image-alt` A link holds only images with no alternate text, so a screen reader has nothing to read out for it. In HTML, any link with nothing to name it, and an image with no alt attribute at all. A warning by default, on an Email and a Page, and on HTML (an `Html` block's and `lintHtml()`'s). **Fix:** give the image an alt that says where the link goes, or the link an aria-label. ### `alt-text` An image's alternate text is a file name, a URL or a word like "image" or "logo", which says nothing about it. A note by default, on an Email and a Page. **Fix:** say what the image shows ("Tarn's logo", "A blue bowl"), or leave alt empty if it's only decoration. ### `link-text` A link or button says only "click here", "read more" or the like: screen readers list links by their words alone. A warning by default, on an Email and a Page, and on HTML (an `Html` block's and `lintHtml()`'s). **Fix:** say where it goes ("Confirm your email"), or give it an aria-label. ### `link-empty` A link or button has no words, alternate text, aria-label or title: nothing names it. A warning by default, on an Email and a Page. **Fix:** give it words, or an aria-label. ### `link-target` A link goes nowhere (empty, #, a mailto: or tel: with no address or number), over http: rather than https:, or (in HTML) runs a script. A warning by default, on an Email and a Page, and on HTML (an `Html` block's and `lintHtml()`'s). **Fix:** link to the page it's for, over https:. ### `heading-order` A heading is more than one level below the heading before it (an h2 followed by an h4). A warning by default, on an Email and a Page, and on a whole HTML document (`lintHtml()`'s). **Fix:** use the next level down, and style it as you want it to look. ### `heading-one` There's more than one level-1 heading. A note by default, on an Email and a Page, and on a whole HTML document (`lintHtml()`'s). **Fix:** keep one level-1 heading, the title, and use level 2 for the rest. ### `heading-empty` A heading has no words. A warning by default, on an Email and a Page, and on HTML (an `Html` block's and `lintHtml()`'s). **Fix:** give it words, or take it out. ### `contrast` Text is too faint against its background to read easily: under WCAG AA's 4.5:1, or 3:1 for large text. A warning by default, on an Email and a Page. **Fix:** darken the text or lighten the background (or the reverse) until it reaches the ratio. ### `text-size` Body text is under 14px, hard to read on a phone. A note by default, on an Email and a Page. **Fix:** make it 14px or larger, on phones at least (style.mobile.fontSize). ### `lang` The email or page doesn't say its language, so a screen reader may read it in the wrong one. A warning by default, on an Email and a Page, and on a whole HTML document (`lintHtml()`'s). **Fix:** give the root a lang ("en", "fr") (in HTML, give <html> a lang ("en", "fr")). ### `dir` The language is written right to left, but the root doesn't say so. A warning by default, on an Email and a Page. **Fix:** give the root dir="rtl". ### `title` The page has no title for the browser's tab and for screen readers. A note by default, on a Page, and on a whole HTML document (`lintHtml()`'s). **Fix:** give the root a title (in HTML, give the email a <title> in its <head>). ### `preview-text` The email has no preview text, or a very short one: inboxes show its first words instead. A note by default, on a Email. **Fix:** give the Email a previewText of 30 characters or more. ### `merge-syntax` Merge syntax ({{name}}, *|NAME|*, %%name%%) is typed into the text, where a MergeTag would write it. A warning by default, on an Email and a Page. **Fix:** use <MergeTag name="…" />, which writes your sending service's syntax (the mergeTag option). ### `unsubscribe` A marketing email (kind: "marketing") has no unsubscribe link. A warning by default, on a Email. **Fix:** add a link to unsubscribe or manage preferences, or the service's MergeTag for one. ### `video-title` A video's title is the default, "Video": the alternate text of its thumbnail in an email. A note by default, on an Email and a Page. **Fix:** give it a title that says what the video is. ### `role-button` A link has role="button": screen readers announce a button, which a link in an email isn't. A warning by default, on HTML (an `Html` block's and `lintHtml()`'s). **Fix:** take the role off: a link is a link, however it looks. <!-- rules:end --></script></main><footer class="site-footer"><div class="wrap"><div class="footer-grid"><div><a class="brand" href="/"><svg width="28" height="21" viewBox="228 326 798 600" aria-hidden="true"><path style="fill: var(--logo-ink, #E3E8ED)" d="M595 388 L623 424 L436 627 L665 913 L522 913 Q492 913 470 891 L262 678 Q240 654 240 627 Q240 600 262 576 L428 410 Q452 388 482 388 Z"></path><path fill="#EF4638" transform="rotate(180 627 626)" d="M595 388 L623 424 L436 627 L665 913 L522 913 Q492 913 470 891 L262 678 Q240 654 240 627 Q240 600 262 576 L428 410 Q452 388 482 388 Z"></path></svg><span>Hyper Elements</span></a><p class="muted" style="max-width:34ch;font-size:14px;margin:14px 0 0">The content engine for agents. Email, web pages, PDFs and images. Open source, MIT.</p><p class="muted built-by">Built by the team at <a href="https://unlayer.com">Unlayer</a></p></div><div><h2>Learn</h2><ul><li><a href="/docs">Introduction</a></li><li><a href="/docs/quickstart">Quickstart</a></li><li><a href="/blog">Blog</a></li><li><a href="/docs/components">Components</a></li><li><a href="/docs/rendering">Rendering</a></li></ul></div><div><h2>Explore</h2><ul><li><a href="/templates">Templates</a></li><li><a href="/playground">Playground</a></li><li><a href="/email-testing">Email testing</a></li><li><a href="/ai">For AI agents</a></li></ul></div><div><h2>Compare</h2><ul><li><a href="/migrate">Migrate from React Email</a></li><li><a href="/compare/react-email">Hyper Elements vs React Email</a></li><li><a href="/compare/mjml">Hyper Elements vs MJML</a></li><li><a href="/alternatives/react-email">React Email alternatives</a></li><li><a href="/alternatives/mjml">MJML alternatives</a></li><li><a href="/alternatives/litmus">Litmus alternatives</a></li></ul></div><div><h2>Project</h2><ul><li><a href="https://github.com/hyper-elements/hyper-elements">GitHub</a></li><li><a href="https://www.npmjs.com/org/hyper-elements">npm</a></li><li><a href="/llms.txt">llms.txt</a></li><li><a href="/llms-full.txt">llms-full.txt</a></li><li><a href="https://github.com/hyper-elements/hyper-elements/blob/main/LICENSE">MIT License</a></li></ul></div></div><div class="footer-note"><span>© 2026 Hyper Elements</span><span>Every preview on this site is real output, rendered by Hyper Elements when the site is built.</span></div></div></footer><script type="module" src="/_astro/Base.astro_astro_type_script_index_0_lang.DDg6wGeA.js"></script></body></html><script type="module">var e=document.querySelector(`.side-mobile`),t=document.querySelector(`.docs-side`);e?.addEventListener(`toggle`,()=>t?.classList.toggle(`open`,e.open));var n=document.querySelector(`.lightbox`);if(n){let e=n.querySelector(`img`),t=n.querySelector(`.lightbox-caption`),r=n.querySelector(`.lightbox-open`),i=n.querySelectorAll(`[data-step]`),a=[],o=0,s=i=>{o=(i+a.length)%a.length;let s=a[o],c=s.querySelector(`img`);e.src=s.href,e.alt=c?.alt??``;let l=s.closest(`.client-gallery`)?.previousElementSibling?.textContent?.trim(),u=s.closest(`figure`)?.querySelector(`figcaption`)?.textContent?.trim();t.textContent=[l,u].filter(Boolean).join(` · `)||c?.alt||``,r.href=s.href,n.querySelector(`.lightbox-body`).scrollTop=0};document.addEventListener(`click`,e=>{let t=e.target.closest(`.prose .client-gallery a`);!t||e.metaKey||e.ctrlKey||e.shiftKey||e.button!==0||(e.preventDefault(),a=[...t.closest(`.client-gallery`).querySelectorAll(`a`)],i.forEach(e=>e.hidden=a.length<2),s(a.indexOf(t)),n.showModal())}),i.forEach(e=>e.addEventListener(`click`,()=>s(o+Number(e.dataset.step)))),n.querySelector(`[data-close]`).addEventListener(`click`,()=>n.close()),n.addEventListener(`click`,e=>{(e.target===n||e.target.classList.contains(`lightbox-body`))&&n.close()}),n.addEventListener(`keydown`,e=>{(e.key===`ArrowLeft`||e.key===`ArrowRight`)&&s(o+(e.key===`ArrowLeft`?-1:1))}),n.addEventListener(`close`,()=>e.removeAttribute(`src`))}</script>