Version 1.2.0

August 30, 2026

Extending this plugin, or reading its tables directly? Some public service signatures and two database columns changed. See UPGRADE.md.

Scan history older than your Retain Scan Results setting is deleted the first time Craft runs garbage collection after this update. That setting never actually deleted anything before now, so a site that has been running the plugin for a while almost certainly holds more history than the setting allows, and the default is 90 days. If you want to keep what you have, raise it (or set it to 0 to keep everything, on Pro) before you update. Scores and trends are drawn from that history.

Added

  • The Overview says when questions are still waiting on you. Potential issues do not count against the score either way until somebody answers them, so a site could read 100 out of 100 with zero open issues while a hundred questions sat unanswered in another tab. The score now says how many are left and links straight to them.
  • When there is genuinely nothing left, nothing failing and every question answered, the Overview says so and marks it with a tick. It holds back until all of it is true, so it can never be mistaken for a passing score with a queue behind it. There is a button beside it for a bit of confetti, since getting a site to that point is a fair amount of work. Neither the tick nor the confetti is offered to anyone who has asked for less motion.
  • The Overview says when a scan is running, and how far through it is. Every figure there is worked out from the latest scan of each page that has one, so a site part-way through a sweep was reporting on the pages done so far with exactly the confidence of a finished sweep: clear the history, start a scan, and three pages in the site read 100 out of 100 with nothing failing. While a scan is working the Overview now says so and gives the count, and the all-clear waits until it has finished.
  • A VPAT remark now tells you when the findings have moved under it. Remarks are stored text and nothing recomputes them, so one written while four occurrences were open still says four after you have fixed them or answered the questions behind them. The row now says what the count was when you wrote it and what it is now, so you can decide whether the wording still holds. It applies to remarks you type yourself as well as drafted ones, since both go out of date the same way, and it stays quiet on anything saved before this release rather than flagging work you have already done.
  • Redrafting a VPAT remark no longer carries stale claims forward, or dresses them up. A remark is stored text, so one written while findings were open stays on the row after you answer them. Redrafting fed that old wording back in as your notes and kept its numbers, and in rewriting it could describe them as having been found by manual evaluation: a count with nothing behind it, wearing a provenance nobody gave it. Drafting now takes its numbers from the current findings, and never says how something was established unless the material says so. If a row's remark is out of date, clear the box before redrafting.
  • Drafting works on the criteria nothing was found against, which is most of the report. Where the scans cover part of a criterion, the draft now writes what testing has and has not established: what was checked, over how many pages, and plainly that the rest is unassessed. It is a statement of your testing position, not a claim that the criterion is met, and the wording holds that line: an untested thing is untested, not passing. Criteria no scanner touches at all still ask you to jot something first, because there the only material is the criterion's own name.
  • Drafted VPAT remarks are written to match how published conformance reports actually read. The drafting was told to use a "professional VPAT register" and left to guess at the rest, so it could produce the two things that make a real report useless: a count with nothing attached to it, or a vague phrase with no scale. It now names what fails and says how much of it there is, leaves out the test method (that belongs to the report, not to one row), does not restate the criterion back at you, and does not hedge without something concrete beside it. It still never picks the conformance level for you.
  • Drafting a VPAT remark with AI no longer counts questions you have already answered. The draft was built from the findings for that criterion without checking whether you had ruled on them, so it could write "the scanner identified 4 instances" into a row whose own evidence line said the scan found nothing. Both numbers came from the same table.
  • No criterion on the VPAT is signed off by the scanner any more, so a clean scan no longer sets one to Supports on your behalf. Five were: Page Titled (2.4.2), Language of Page (3.1.1), Contrast (1.4.3), and, once server-side browser scanning was switched on, Non-text Contrast (1.4.11) and Target Size (2.5.8). The scan establishes that a page has a title, that the html element carries a lang attribute, and that the text it can measure has enough contrast. The criteria ask for a title that describes the page, a lang value naming the language actually written, and adequate contrast including text drawn into images, and no scanner settles any of those: a site titling every page with the site name passed, so did lang="en" on a page of Irish, and text inside an image has no colour to measure. Target Size makes the case on its own, since a real violation could sit on your Overview while the VPAT called the same criterion Supports. All five now wait for you, with what the scan did cover shown beside them as evidence. If you have already signed any of them off, your answer stands and nothing changes.
  • Every criterion on the VPAT now shows what the scans covered and what is left for you. Automated testing reaches about a third of WCAG, so most of the report comes down to a person deciding, and each of those rows was a dropdown and an empty box: to answer one you first had to go and work out for yourself what had already been tested. On a real site that is around forty rows of it. Each row now says what was checked, over how many pages, how many findings are still open, and plainly what no scanner can tell you, so the ones that need five seconds are easy to tell from the ones that need an afternoon. Criteria nothing automated touches say so too, which saves you looking.
  • Browser findings now say why the element failed, not just which rule it failed. Every one of them carried the rule's own statement, which is the same sentence on every occurrence of that rule anywhere on the site. Touch target size is the clearest case: "All touch targets must be 24px large, or leave sufficient space" covers three unrelated jobs, since the target can be too small, or big enough but sitting too close to its neighbours, or covered by something else like a floating back-to-top button. The finding now names which of those it is, so a link that looks perfectly large is no longer reported with a sentence that reads as a contradiction.
  • Alt text that runs long now tells you by how much. The finding says the actual length and how far over the 150 guideline it is, so you can trim to a number instead of counting a truncated preview back by eye.
  • The alt text field counts as you type, and says how far past the 150 guideline you are once you go over. On the Assets page and on the asset's own edit screen, which is where most alt text actually gets written.
  • New rule for cells, links and buttons whose whole announced name is a symbol: a tick, a cross, an arrow, or a dash standing in for "not applicable". The shape carries the meaning and the character does not, so a screen reader either says "check mark" or, at the symbol verbosity most people leave set, says nothing and reads an empty cell. Comparison tables are full of this. The fix is visually hidden text saying what the symbol means, with the symbol itself marked aria-hidden, and the rule goes quiet as soon as you do that.
  • Questions that only come up at one screen width now say so on the row. A page is measured at both desktop and mobile, and the preview shows one at a time, so a decoration that clears the text on a wide screen but sits behind it on a narrow one produced a question you could not see anything wrong with. Where the same question comes up at both widths it is asked once, not twice.
  • A check for HTML that renders inside a code sample instead of being shown as text. Documentation writes about tags, and <code> is where they go, but <code> is presentational: it escapes nothing. Write `<code><iframe src></code> and the reader gets an actual iframe rather than the three words you typed. The page that comes out is perfectly valid, so no validator and no other checker says a word about it. What it costs depends on the tag. A void one like ` takes the sentence with it. One that is not void takes the rest of the page, because the browser hands it everything up to a closing tag that never comes, and paragraphs, tables and whole sections stop existing while the page still returns 200 and looks fine until somebody scrolls. Those are reported as errors and the finding says roughly how much never rendered; the rest are warnings. Syntax highlighting and links inside a sample are left alone.
  • Contrast is now measured in states the page is never in while it is being scanned: hover, focus and text selection. Every automated checker reads the rendered page, and a rendered page is always at rest, so a hover colour that fails, a focus colour that fails, or a selection colour nobody can read has never shown up in an audit. These are read from the stylesheet instead and measured against the background the element actually sits on. Each state is its own rule (contrast-hover, contrast-focus, contrast-selection) so you can ignore them separately, and the finding says which state it is about rather than leaving you hunting for a failure you cannot see. A rule whose colours cannot be worked out, a var() this pass cannot evaluate for instance, is skipped rather than guessed at, and a rule inside a media query that does not apply at the scanned width is left alone.
  • The accessibility statement can now be dropped into a page that already has a heading of its own. Pass a heading level and the statement titles itself at that level, with its own subheadings stepping down from there, so you are not left with two competing h1s on the one page. You can change the title text the same way: craft.a11y.accessibilityStatementHtml(null, { headingLevel: 2, title: 'How accessible this site is' }). Left alone, it renders exactly as it always did.
  • AI alt text works on SVGs. The API takes raster formats only, so an SVG was sent as-is, refused, and reported as a rejected request with no hint that the format was the problem. Vectors are now rendered to a PNG first, at a size worth reading rather than whatever the icon declared. Where the server cannot render one faithfully the plugin says so plainly and asks you to write that one by hand, rather than describing an icon it could not actually see.
  • craft.a11y.isDecorative(image) for front-end templates, so an image macro can render an empty alt and role="presentation" for images you have marked decorative. Asking per image costs one query for the whole page rather than one per image, so it is safe inside a loop. craft.a11y.decorativeAssetIds() hands over the whole set for a template that would rather hold it itself.
  • The Statement and the VPAT now say which scan their figures came from, and that fixing a page does not move them until it is scanned again. Both are worked out from scan data, so a count that stays put after you have fixed something reads as the report being stuck rather than as it waiting on a scan.
  • A Save all drafts button on the Assets page. Generating all the alt text left you clicking Save on every row one after another, which on a full page of images is a lot of clicking for something you have already reviewed. The button appears beside Generate all as soon as there is a draft waiting, shows how many, saves them one after another, and reports back once at the end rather than a notice per image. It picks up anything you have typed by hand as well, not just what the AI drafted, and skips any row you have left empty. (#6)

  • A new check for block content nested inside a paragraph. Wrapping a rich-text field in a styled paragraph, <p class="text-lg">{{ entry.body }}</p>, produces a paragraph inside a paragraph, and the browser quietly closes the outer one the moment it meets the inner. Your classes go with it and the text renders unstyled. Nothing is missing and nothing is mislabelled, so no other tool has a word to say about it: every scanner works from the parsed page, and the parser has already tidied the evidence away. This one reads the source instead. The report names the tag that closed the paragraph and tells you the two ways out, either unwrap the field or make the wrapper a div. It is reported as a warning and against no WCAG criterion, since the one it used to fall under was dropped in WCAG 2.2.

  • Scanning of pages that have no element behind them: search results, filtered listings, paginated archives. Craft routes plenty of pages it does not back with an entry, and the sweep works off elements, so those pages were simply never looked at. List them under Settings and they are scanned along with everything else, or scan one on its own with craft accessibility-audit/audit/scan-url --url=/search?q=craft. They appear in the Pages list and get their own report like any other page, named by the page title rather than the address, and you can answer their potential issues the same as anywhere else.

Changed

  • The identical links check now judges how strong each link's surroundings actually are, rather than counting any two different regions as enough. A named region is real context; an unnamed one that leans on the heading above it is weaker and is now reported as needing a fix rather than passing; and a heading that just repeats the link text is no context at all, which is a common case that used to pass silently. A pair is graded by its weaker side.
  • Where a link sits in a region with no name, the report now offers naming that region as the second fix, ahead of editing each link. One attribute settles every ambiguous link inside it at once and changes nothing about what any link announces. The region is named in the report by its tag, its classes and how many flagged links it holds, so it can be found in a template.
  • "Are these identical links going to different places?" now says which kind of problem it found, because they are not all the same problem. Two links reading the same only breach WCAG 2.4.4 when nothing around them says which is which, so the check reads where each one sits and splits the verdict: same place with nothing between them is a failure, different places with one of them unnamed is a failure and the missing name is the fix, and different named places pass at AA while still reading as two identical entries in a screen reader's links list. It also weighs how far apart the destinations really are: two links to different sections of the same page are a tidiness point rather than a breach, which in documentation is most of what this check used to find. The finding prints where each link lives, an unnamed landmark shows as such because that is usually the real defect, and it offers the fixes in order, starting with changing the visible text. It also warns against reaching for aria-label on the link, which replaces the announced name instead of adding to it and breaks voice control.
  • Running a readability analysis now needs the Run scans permission rather than View reports. Analysing fetches the page from your server and, with AI suggestions on, calls the Anthropic API, so it spends your outbound requests and your API budget: that is the scanning permission's job, not the reading one's. Reading the results is unchanged and still only needs View reports. If you have editors who analyse pages, give them Run scans. The Analyse and Re-analyse buttons no longer appear for anyone without it, rather than appearing and failing.
  • Opening Readability from an entry's accessibility panel now fills the page URL in for you. You were being handed an empty field for a page the plugin already knew about. (#4)
  • The accessibility panel now sits at the top of the element sidebar, above the panels other plugins add (SEOmatic's among them) and below Craft's own status and meta. It is a panel you act on rather than read, so it should not be buried.
  • Edit element on a page report opens in a new tab, the same as View page beside it, and carries the same icon. Working through a list of pages, you were losing the report every time you went to fix something. (#2)

Fixed

  • Dismissed contrast questions stay dismissed. Two things were giving one element two identities, so an answer given to one never reached the other. The report marks an element in its preview when you click Show on page, and the browser pass then read that same preview and recorded the element with the plugin's own mark on it, as though it were a different element. And the engine reports the whole element when its markup is short but only the opening tag once it passes a certain length, which a syntax-highlighted code block crosses partway through rendering. Occurrences are now identified by the opening tag with the plugin's own marks removed, and migrations bring existing answers onto it.
  • The accessibility statement no longer tells a fully scanned site that nothing has been scanned. It worked out whether scan data existed by looking at what the scans had concluded, and once no criterion is signed off by the scanner there is nothing to conclude on a site with nothing failing. A clean site was told its compliance status rested on no evidence at all. It now asks whether pages have been scanned, which is the actual question.
  • The Overview heading over the rules worth fixing said "Fix these 10 issues" whatever was actually listed, including when the list was empty. It now counts what is there, and says so plainly when there is nothing.
  • The accessibility statement and the VPAT no longer count questions you have already answered, or issues you have already fixed, against a success criterion. Conformance levels were read off the findings with no filtering at all, so a question dismissed weeks ago still held a criterion at Partially Supports, and so did an issue resolved since. The statement could show three failing criteria while Issues listed one rule, with nothing on either screen to explain the other two. Since a statement is a public claim about your site, it now reads the findings the same way every other screen does: dismissed questions and fixed issues are spent, confirmed ones still count.
  • The statement and the VPAT were also throwing away the findings of every page scanned by address, keeping only one of them, so a site with pages listed under Additional URLs was drawing its conformance from an incomplete picture.
  • An entry whose address redirects somewhere else is no longer scanned as if it were a page. A section landing page that sends readers to its first child has no page of its own, so what came back belonged to the child, and the child is scanned in its own right: one page was being filed under two names, with the findings doubled and both counting against your page limit. Redirects that are the same page arriving differently, http to https, a trailing slash, a tracking parameter picked up on the way, carry on being scanned as normal.
  • A page report could show you an older scan than the one you had just run, which made answered questions look like they had come back. Scan times are stored to the second, and a re-scan writes its row while the browser pass is still working on the same page, so two scans regularly share a second. The report picked between them at the database's discretion, and the older one predates whatever you had answered since.
  • Dismissals made before this release on pages with generated ids are rescued rather than left dead. A ruling was keyed on the markup it was made against, ids and all, so a form field that comes back with a fresh id every render could never be matched again. Taking ids out of the key fixes everything from here on, but not what was already stored, so a migration recovers the original markup from the scan history and moves each ruling onto the new key. Where you have since answered the same question again, the newer answer is kept and the stale row is cleared.
  • Listings now show a page's address as well as its title. Titles are not unique: a plugin's landing page and its support page usually share one, share a layout, and so throw up the same findings off the same shared component. Dismissed rows for two different pages looked like the same row twice, which made a ruling that had held look like one that had not. The Dismissed tab and the page report both carry the address now.
  • Contrast findings answered from the page report stay answered. That pass rebuilds its results every time the report opens, and it was not carrying your answers onto the new rows, so anything you had confirmed or waved through came back the next time you looked at the page.
  • Dismissals stick on pages built with Formie, and anything else that mints a fresh id into every element each time the page renders. The same field came back with a different id on every scan, which made it a different occurrence as far as the plugin was concerned, so a question you had answered was asked again after the next scan. Ids are no longer part of what identifies an occurrence. Anything you have already dismissed stays dismissed: the old keys are still checked.
  • The Pages with Issues tab now lists pages with issues, in the count beside the tab and in the rows underneath it. It was listing every page that had been scanned, so a site with nothing wrong showed hundreds of rows with a dash where the counts belong, under a heading counting them all as pages with issues.
  • The Issues tab no longer tells you to run a scan when you have just run one and passed. A site nobody has scanned and a site with nothing failing both leave the list empty, and they now say which one you are looking at.
  • Contrast questions are no longer raised about text the scanner never had in view. Anything running off the side of the screen, or sitting further down a long page than the scan reaches, came back as "another element covers part of it", which sent you looking for a layering problem that was not there. Worse, which elements it landed on shifted from one run to the next, so answering one never cleared the queue: a different one took its place on the next scan. There is no answer a person can give to "the scanner could not see this", so it is not asked. Nothing that was measured and failing is hidden.
  • Visually hidden text is no longer reported as a contrast failure. Text put where a screen reader reads it and an eye never sees it has nothing on screen to have contrast with, but the modern way of hiding it leaves a pixel of box behind, so it was being measured like anything else. A category page with twenty cards could report forty failures nobody could see or fix. Covers the clip-path and clip rect forms, and hiding applied to a wrapper rather than to the text itself.
  • The alt text field on the Assets page no longer stops you at 125 characters. Craft's own alt field has no limit, and the plugin's own check does not complain until 150, so the field was cutting people off part way through fixing a finding it had raised, at a number nothing else used. 125 is what the AI generator is asked to aim for, which is a different job, and it still does.
  • A page that redirects off the site altogether is skipped and says so. What is at the other end belongs to somebody else, so auditing it tells you nothing about your own site, and it would have put a foreign address in your listing counting against your page limit. Redirects that stay put carry on as normal: http to https, trailing slashes, path moves, a hop down to a subdomain, dropping a leading www, and moves between sites this install serves.
  • A page that redirects is now recorded against the address it ends on. Asking for the old address filed the new page's content under the old one, so the same page sat in the listing twice under two names, with the same findings, both counting against your page limit, and answering a question on one left the other still asking.
  • Pages that are missing or broken are skipped on purpose rather than by accident, and the report says which status came back. A page returning 404 and a server that could not be reached at all used to arrive with the same sentence, so there was no telling one from the other.
  • Scanning an entry whose page could not be read no longer reports it as scoring 100. Nothing was read, so there is no score, and the console now says why it was skipped instead of printing a green 100 for a page nobody opened.
  • A rule page now describes the rule instead of quoting one page's findings as though that were the definition. Where there is no description written yet you still get an example, but it is labelled as one.
  • Contrast questions now say why they were asked. axe tells us what stopped it measuring, and that reason was being worked out and then thrown away, so every question read the same and there was nothing to tell you where to look. You now get the actual reason next to the markup: a background image, a partly transparent text colour, something sitting over it, and so on.
  • Contrast questions are no longer raised about text that was not visible on the page. There is nothing to see, so there was nothing to answer.
  • "Show on page" now finds occurrences whose markup was too long to store whole. Anything with a long list of utility classes was getting cut inside the class attribute, and the report was matching on the half-read list, so it could never locate the element and told you the page must have changed.
  • Dismissing a whole group is faster and no longer answers with a blank error page when something goes wrong. Every occurrence in a group belongs to the same scan, and each ruling was working that scan's score out again from scratch, so a group of fifty meant fifty recalculations of the same number while you waited. It is worked out once at the end now. If a ruling does fail part way, you get a sentence saying how many were saved before it stopped, with the detail in the logs, instead of a server error that leaves you guessing whether any of it landed. The ones that did save are real and stay saved.
  • Dismissing a contrast question did not stick. "Does this text have enough contrast against what is behind it?" comes from the browser pass, and that pass rebuilds its findings from scratch every time it runs, so the next scan put the question back as though nobody had answered it. Answers are kept in their own table precisely so they outlive a scan, and the browser pass now carries them forward the way the page scan always did. Answers you have already given apply again on the next scan; nothing needs re-doing.
  • Adding a non-accessible content entry from a scan suggestion filled the statement in with the wrong things. "What is affected" asks for what a member of the public would recognise, and it was given the WCAG criterion name, which names the rule rather than the thing on the page somebody cannot use. "Why" was given the criterion's own wording, and that wording states the condition for passing, so a statement saying a page does not comply carried a sentence underneath describing the site working correctly. This is a document with legal weight, so it now fills in only what the scan established, the criterion and how many issues were found, and leaves the description to you. Each entry is headed with the criterion it came from, so a list of them is still something you can tell apart. The field also shows an example of the sort of answer it wants, matched to the criterion: greyed guidance that is never saved, so nothing reaches the published document unless you wrote it.
  • Standard-edition sites were charged against their page limit for pages they had deleted. Craft soft-deletes, so a deleted entry keeps its row and the scan attached to it survives, and the count behind the limit was reading every scan ever taken. A site that had scanned and then deleted its way past the limit was refused new scans on the strength of pages that no longer exist. Deleted pages no longer count. Nothing needs clearing out by hand: the count is worked out fresh each time.
  • Show on page could not find an element that was sitting in plain view. The browser engine cuts the markup it reports at a fixed length, and on utility-class markup that cut lands inside the class attribute, leaving a half-finished class name that matches nothing and no text to fall back on. A heading with a long list of classes was the usual victim. The half-finished name is now dropped and the rest of the classes are compared as far as they go.
  • Twelve strings in the control panel had no entry in the message file, so they stayed in English on a translated install while everything around them changed. There is no warning when that happens, and an English install looks right either way, so they are now checked automatically.
  • The review queue no longer asks the same question dozens of times. A check that runs per element runs once per element, so a reference table whose cells could not be measured for contrast filled the page with near-identical cards, every one showing the same truncated markup because that is genuinely what the markup says. Repeated occurrences are now gathered into one card that says how many there are, and answering it answers the lot. They are still listed underneath for anyone who wants to take them one at a time, and each one still keeps its own answer, so nothing is decided on your behalf. Show all on page frames every one of them at once.
  • Contrast was being questioned on markup you had already marked as decoration. An arrow or icon in an aria-hidden="true" span is not announced and says nothing the text beside it does not, which WCAG treats as pure decoration and exempts. The plugin's own contrast pass has always skipped those, but axe measures them, so one engine asked about a node the other had deliberately passed over. Both now leave them alone. Findings already recorded against decorative markup are cleared on update and the scores they affected are worked out again. An aria-hidden="false" element is announced, so it is still checked.
  • "Are these identical links going to the same place?" was asked about links that are not identical at all. The check compared the text you can see, so a row of client cards each with a "Visit Website" button looked like the same link repeated, when every one of them carried an aria-label naming its destination. A screen reader reads those out as different links, which is the whole of what 2.4.4 asks for, so there was never a question to answer. It now compares what is actually announced, the same as the other link checks were taught to do. Visually hidden text counts towards the name and an aria-hidden decoration does not, so two "Read more" links that differ only in a hidden span are left alone, and two that differ only in a decorative arrow are still reported.
  • Filename alt text is now caught on the page, not just on the asset. Craft names an asset after its file, so a template reaching for the title rather than the alt field ships "Asset7623" for asset7623.jpg while the Assets page shows the image as perfectly fine. The alt on the page is now compared against the image's own filename, so the two cannot disagree quietly. A descriptive alt that happens to match a well-named file is left alone.
  • A link that warns about opening a new tab in visually hidden text was reported as though it had no warning at all. Only the aria-label and title were being read, and hidden text inside the link is announced just the same, so the plugin was flagging the very pattern its own documentation recommends. The whole announced name is read now. A link whose aria-label replaces that hidden text is still flagged, because in that case the warning genuinely is not announced.
  • The Assets page could report a missing alt text it then refused to show you. An image in the trash still counted towards the missing-alt figure, while the list below it correctly left trashed images out, so the chip said one and the page said none. Trashed images no longer count.
  • Contrast findings recorded against unstyled pages by the bug below are cleared out on update, and the scores they dragged down are worked out again. A genuine failure is untouched, and would come back on the next scan in any case.
  • AI alt text now works on very large images. Anything past 8000 pixels on a side was refused outright by the API, so print-resolution scans and untouched camera originals never got alt text at all. Large images are scaled down before they are sent, which fixes the refusal and cuts the upload, and costs you nothing in quality: anything bigger was being scaled down at the far end anyway. (#3)
  • A link carrying an aria-label was still being judged on the text you can see inside it. A button reading "View" with an aria-label of "Full recipe at ohmydish.com (opens in new tab)" got reported for vague link text, when the label spells the destination out plainly and is what a screen reader announces. Links are now judged on the name that is actually announced. The same pass taught the scanner to read a name from an SVG title or an aria-labelledby reference, so links named that way are no longer reported as having no name at all. (#7)
  • Contrast findings on repeated markup all pointed at the same element. A row of category links, a list of tags, any component that appears more than once: every occurrence was recorded against a selector built from tag and class alone, so the whole lot shared one selector and clicking any of them highlighted whichever came first. The report was right about how many there were and wrong about where. Occurrences now carry their position among matching siblings, so each one points at itself. Existing findings pick this up on the next scan.
  • Clicking an occurrence in the Inspect view could box the wrong element when two of them shared an attribute. A page with two links to the same address, a pill near the top and a photo credit further down, framed whichever came first rather than the one the finding was about. The stored snippet is capped in length, and whatever the cut landed in was being compared whole, so the class and text that would have told the two apart were thrown away. They are now read as far as they go, and where there is genuinely nothing left to tell candidates apart the report says it cannot place the occurrence rather than pointing at the wrong one.
  • A vague link whose label only added a new-tab notice stopped being reported. Judging links on the name a screen reader announces was the right call, but "here (opens in new tab)" is still the word "here": the notice describes what the link does to your browser, not where it goes, and judged whole it looked specific enough to pass. The notice now comes off before the check, so those links are reported again while a label that genuinely names the destination is still left alone.
  • The Inspect view boxed every link on the page when you clicked one of those link findings, correctly labelled ones included. It now frames only the links the finding is really about. (#7)
  • AI alt text described the wrong thing on screenshots. Asked about a picture of a control panel, it would describe whatever photo happened to be sitting inside that screenshot, so an image showing you a button came back as a museum gallery or a mountain bike. Fluent, accurate about the pixels, and no use at all to somebody who cannot see the screen. It is now told to name the screen and the controls and to ignore the sample content inside them. (#10)
  • AI alt text could run past its own length limit and then get flagged by the plugin's own review queue, which asked whether the alt text it had just written was too long. The model is asked for 125 characters but counts them poorly, and nothing was checking. An answer that overshoots now gets one more go at saying it shorter, and is trimmed back on a word boundary if it still will not fit.
  • The Generate button on an asset produced weaker alt text than the same image put through the queue. The queued job was passing the filename and title along as context and the button was not, so it had less to go on for no good reason. Both go the same road now.
  • Running the overlay inside Craft's preview pane could file the results against the draft you were previewing rather than the entry, so the scan looked like it worked and then never showed up on the page report. The overlay still runs in a preview and still shows you what it finds, but it no longer saves anything, and the panel says so. (#9)
  • Text that only appears on hover was being checked in its hidden state. A photo caption that fades in over the image sits in the page the whole time, so it was measured against the page background rather than the photo it appears over, and reported as failing on colours it never actually shows. Fully transparent text is now left alone until it is the state on screen.
  • Pages that inline their critical CSS and load the full stylesheet afterwards were reported as a wall of colour-contrast failures against text that is perfectly readable. Between the two, the page has its backgrounds but not its colours, and anything coloured through a CSS variable, which is how Tailwind ships its palette, falls back to the browser default link blue. The contrast pass now waits for the page to stop taking on styles before it reads a single colour, the same wait the other browser checks already took. Existing findings clear on the next scan.
  • Re-scanning a page report the moment it opened could file a set of findings that were not about the page at all: no title, no language, no main landmark, no heading. Those are all true of the blank frame the preview shows while it is still loading, which is what was getting checked. The checks now wait for the page itself.
  • The Retain Scan Results setting did nothing on its own. See the warning above before updating: the first prune clears whatever has built up in the meantime. Nothing read it that deletes: the only thing that pruned anything was a console command, so unless you had wired that into cron yourself, scan history grew without end no matter what the setting said, and the settings screen told you it was automatic. Pruning now rides along with Craft's own garbage collection, so the setting means what it says and there is nothing to schedule.
  • The Overview could show 100 while the card underneath it said criteria were failing. Those figures are page scores averaged over the site, so on a large one a few failing pages move the average by a fraction of a point and it rounded up to full marks. A hundred percent beside "3 criteria failing" reads as a conformance claim, and it is not one the evidence supports: the statement page refuses to make that claim on the same evidence. The average now stops at 99 unless every page really is clean.
  • Re-scan on a page report only checked the width you happened to be looking at. The report runs the browser checks in its own preview rather than queueing them, and that preview only ever measured the viewport on screen, so the other one kept showing findings from the previous scan until you switched to it and re-scanned again. One Re-scan now walks both widths and puts the preview back where you left it.
  • The accessibility panel on an entry pushed its dividing lines out past its own edge on narrow screens, leaving a sliver of sideways scroll. The panel now follows the sidebar's own spacing at every width. (#5)

Security

  • Outbound fetches now connect only to the addresses the safety check validated. Checking a hostname and then handing the URL to an HTTP client leaves a gap, because the client looks the name up again when it connects, and a name someone else controls can answer with a public address for the check and a private one for the connection. Re-checking the name does not close that. The validated addresses are now given to curl directly, and redirects are followed one checked hop at a time rather than by the client, so every hop gets the same treatment. Reaching this needed the Run scans permission and a URL the site already scans, so it was never open to the public.

Version 1.1.0

August 21, 2026

Out of beta. Seven beta releases and five weeks of scanning real production sites got the plugin here. There are no breaking changes and no migrations; anyone on a beta build should simply update.

Added

  • The needs-review queue on a page report now takes bulk rulings: tick the occurrences (or Select all) and Dismiss selected answers the lot in one go. Built for the page where one judgment repeats fifty times, a sticky-nav's links all flagged for the same unmeasurable background being the classic case.

Fixed

  • Show on page did nothing for the alt-text questions ("Is this alt text too long?"): the stored snippet is the alt text itself, which no text search can find because images have no text. The image is now matched by its alt attribute, so the button lands on it like any other finding.

Changed

  • On the needs-review cards, Not an issue is no longer a solid red button with Confirm as failure tucked beneath it as a small link. The two rulings now sit side by side with equal weight, with Confirm as failure carrying a red label for its consequence. Dismissing should never look like the main event when it is the ruling that costs nothing.

Version 1.0.11-beta.1

August 21, 2026

Added

  • Pro: the admin overlay now works on decoupled frontends. If your site is headless (Next, Nuxt, Astro or the like), Craft never renders your pages, so the overlay could never appear there. Now you add one script tag to your frontend, generate a token under Settings → Tools, and open the site through an activation link: the overlay runs on your real frontend with the same axe engine and rules, and stores results against the same scans you see in the control panel. The script does nothing at all for ordinary visitors, so it is safe to ship in production builds, and it suits heavily cached sites just as well: pages served from Blitz or a CDN edge never run Craft, so this is the way to get the overlay onto them too. The new Decoupled Frontends page in the docs has the setup.
  • The common consent-management banners (OneTrust, Cookiebot, Ketch, Usercentrics, Didomi, TrustArc, Osano, Complianz, CookieYes, Iubenda, Termly) are now excluded from every scan surface. Their markup is third-party UI you can neither fix nor keep still between scans, so findings inside them only buried your own. A new Excluded Elements setting under Settings → Scanning takes extra CSS selectors, one per line, for anything else on the page you do not control: chat widgets, embedded players, testing overlays.

Fixed

  • Clicking a finding whose element could not be located in the Inspect preview used to box every element the rule could apply to: one nameless button reported, eighty-odd buttons highlighted; two misplaced list items, every list item on the page. Matching now finds the right element far more reliably, snippets cut off mid-tag included, and when the element genuinely is not in the preview (added by a script that does not run there, say), the report says it cannot highlight instead of highlighting everything.
  • Browser-pass findings were described with axe's rule blurb ("Ensures <dl> elements are structured correctly"), which reads like a question about whether anything is wrong. They now carry the requirement that actually failed ("<dl> elements must only directly contain properly-ordered <dt> and <dd> groups…"), the same wording the overlay always used. Applies to newly scanned results.
  • The frontend overlay's Highlight now scrolls to an element that is actually on screen when a finding matches several, and says so when everything it flashed sits inside a collapsed menu or panel.
  • The site's own styles could bleed into the frontend overlay panel: a theme's heading font landing on the panel title, decorative flourishes attached to headings, that sort of thing. The panel now pins its own typography and blocks the page's decorative styles, so it looks the same on every site.
  • On the Inspect page, clicking a finding about something the page lacks (no skip link, no meta description, no landmarks) would box unrelated elements in the preview, as if they were the problem. A missing skip link, for one, highlighted whatever ordinary in-page anchor links it could find. Those findings now show their message without highlighting anything, since there is no offending element to point at.
  • The Inspect preview could highlight the wrong link when two links point at the same URL, a nav item and a "View all" button being the classic pair: the finding was about one, the box landed on the other. The box now lands on the one the finding is actually about.
  • Findings about the document itself (a missing or empty page title, a missing language attribute) no longer try to highlight anything in the Inspect preview either: their reported element is the page as a whole, so the box would land on the whole page or on nothing visible.
  • When a finding landed on an element with no attributes, every occurrence rendered as the same bare tag: a page with dozens of orphaned list items showed dozens of identical <li> chips, with no way to tell one from another. Each occurrence now shows a short preview of the element's text, so you can tell which is which.
  • Not an issue and Confirm as failure could silently fail on findings whose snippet spans multiple lines: the ruling looked saved, then the question came straight back after the reload. Rulings on those findings now stick. If one bounced back on you before, click it once more after updating.
  • Settings pages now render read-only on installs where administrative changes are disabled (a standard production lockdown), instead of refusing to open at all. The usual Craft read-only notice appears, every field greys out, save and token-generation controls are withheld, and both the plugin's own Settings link and the one under Settings → Plugins keep working on those installs.
  • Two elements with an empty id="" were reported as a duplicate id. An empty id cannot be referenced by anything (labels, aria-labelledby, fragment links all need a value), so it collides with nothing; the check now ignores empty and whitespace-only ids. Genuine duplicates are still reported.
  • Highlighting an element that sits inside a collapsed menu or panel looked like nothing happened: the box was applied, but out of sight until the menu was opened by hand. A notice now says where the element is hiding, and the highlight is waiting there when you open it.
  • Show on page could look like it did nothing when the finding's content renders in more than one place, a hero title repeated on a listing card being the classic case: the box landed on whichever copy came first in the page's markup, possibly nowhere near where you were looking, or on a hidden one. All identical renderings are now boxed together, visible ones are preferred, and the preview scrolls to one that is actually on screen.
  • Show on page for an image question could highlight every image sharing an upload path: the stored snippet cut the image URL short, so listing thumbnails all matched it. The right image is now identified, and rulings you have already made keep holding after the change.

Version 1.0.10-beta.1

August 21, 2026

Fixed

  • The Accessibility Audit link on Settings → Plugins gave a 404 instead of opening the plugin settings. The settings pages themselves were always fine, and reachable through the plugin's own sidebar, but the standard route in from the Settings page was broken. It now lands you on the settings like any other plugin. (#1)

Version 1.0.9-beta.1

August 13, 2026

Added

  • A new Browser Settle Time setting under Settings → Scanning controls how long the browser pass waits after a page loads before running its checks, so late-rendering JavaScript can finish. It has always waited 2 seconds, and that is still the default, but the wait is paid on every pass, so on a big site it adds up to hours. Plenty of sites are fine at 500 milliseconds, and you can set 0 to skip the wait entirely.

Changed

  • The browser pass now renders the desktop and mobile checks for a page in one Chrome session instead of starting a fresh one for each. On a site with thousands of pages that halves the browser starts, which takes a serious chunk off the total scan time. Nothing changes in the results themselves.

Fixed

  • On large sites, a queued site-wide scan could quietly miss some pages and scan others twice, because the database was free to hand the pages back in a different order for each batch. The sweep now works through pages in a fixed order, so every page is scanned exactly once.

Version 1.0.8-beta.1

August 12, 2026

Fixed

  • Server-side browser scans could crash partway through a page on servers with limited shared memory, a common setup on containers and managed VPS hosting. Chrome is now told to keep its working memory out of the shared memory area, so those scans complete instead of dying quietly.

Version 1.0.7-beta.1

August 7, 2026

Added

  • You can now point the scanner at a Chrome running somewhere else instead of installing one on your own server. Set Settings → Scanning → Remote Chrome Endpoint to a browserless account, a container of your own, or anything else speaking the DevTools protocol, and the browser pass runs there. This is the only way to get server-side browser scanning on hosts where you cannot install a binary, Craft Cloud among them. Store the URI in an environment variable if it carries a token.
  • Contrast that axe-core could not measure now lands under Needs review instead of being thrown away. axe hands a node back undecided when it cannot work out what is actually behind the text, which happens when another element sits over it, or the text is on an image or a gradient. Those results used to vanish, so a page could look clean on contrast while the hardest parts of it had never really been checked. They now arrive as a question, with the reason axe gave and the ratio the text needs, and they stay out of your score until you confirm one.

Changed

  • storeAxeIssues() takes a fourth argument, the undecided results from axe, defaulting to an empty array. Existing calls keep working unchanged.

Fixed

  • On sites using modern CSS colour syntax, which means any site built with Tailwind 4, the contrast check misread colours it could not parse. A button with its own white background could be reported as failing against the section colour behind it, and text whose colour could not be read was skipped altogether, so genuine failures went unreported. Colours are now read in any syntax the browser understands, oklch included.

Version 1.0.6-beta.1

July 27, 2026

Fixed

  • The accessibility statement preview and the published statement failed with a template loading error, because the built-in statement template was missing from the release package.

Version 1.0.5-beta.1

July 27, 2026

Fixed

  • On the accessibility statement, the Add an entry button and the scan-suggestion chips only saved the statement without adding the entry when the non-accessible content list was still empty.

Version 1.0.4-beta.1

July 27, 2026

Changed

  • Programmatic bulk resaves (the resave commands, migrations) no longer queue a scan per element. Use Scan All for a deliberate site-wide sweep.

Fixed

  • A scan could fail entirely on a page whose alt text or markup contained multibyte characters (curly quotes, accents, emoji) near a truncation point, stalling a site-wide scan with a database error. Truncation is now multibyte-safe, and issue text is sanitised before storage so one bad string can never fail a scan.
  • Saving an entry queued duplicate scans and browser checks for the same URL, because the entry's revision was scanned alongside it. Revisions are no longer scanned.

Version 1.0.3-beta.1

July 27, 2026

Fixed

  • Page report highlights no longer repaint the element's background, and the contrast check ignores the report's own highlights and badges.
  • The page report's contrast and axe passes now wait for JavaScript-injected stylesheets to apply, so styled elements are no longer reported at browser-default colours.

Version 1.0.2-beta.1

July 27, 2026

Added

  • Colour-contrast findings in the page report now show the failing element's markup.

Fixed

  • Page report highlighting now pinpoints the exact elements for duplicate-id and colour-contrast findings. Re-scan a page to update stored contrast findings.

Security

  • Pages rendered for a logged-in admin with the frontend overlay enabled are no longer cacheable, so a full-page cache such as Blitz or a CDN cannot serve the admin's overlay to visitors.

Version 1.0.1-beta.1

July 27, 2026

Changed

  • License type and some supporting github issue docs

Version 1.0.0-beta.1

July 12, 2026

Added

  • Initial release.