Accessibility statement and conformance report
Accessibility at Latchkey
Our own conformance report for our own product, with the issues we know about and have not fixed yet, the surfaces we have not evaluated at all, and the check that runs against this site before a release.
- WCAG 2.2 Level AA target
- 8 surfaces in scope
- 4 open issues listed
- 6 recorded evaluation gaps
- Re-evaluated every 90 days
Our commitment, in one paragraph
We build accessibility tooling, so we hold our own surfaces to the standard we sell against: WCAG 2.2 Level AA. We check them with the same engine our scanner runs before a release goes out, and a state our harness cannot reach is reported as skipped rather than counted as passing. We publish what we have not fixed, below, and what we have not evaluated at all. And we answer a report about a barrier on this site from anyone, whether or not you are a customer.
One thing to be clear about before the detail: this page does not claim that latchkey.app is fully accessible, and it is not a compliance certification. Automated checks cover roughly one third of WCAG success criteria, which bounds what any gate of this kind can tell you, ours included. What automated testing cannot find applies to this site exactly as it applies to yours. Every claim below uses the same five values a procurement reviewer expects, and no surface claims Supports, because a claim of Supports needs a person driving assistive technology behind it and we have not done that yet.
What we evaluated, and how
One row per surface. The claim is one of five values and nothing else: the registry this table is generated from cannot hold another word, and the build fails if it did. The Method cell says what was run, at what widths, on what date, and what was not run, because a one-word method is how “tested” comes to mean nothing.
| Surface | Standard and level | Conformance claim | Evaluated on | Method | Known issues |
|---|---|---|---|---|---|
| latchkey.app marketing site, the free scanner and its result screens | WCAG 2.2 Level AA | Partially Supports | 8 September 2026 | axe-core 4.10 driven by scripts/audit-self.mjs against the built site at 1440, 1200, 375 and 320 CSS pixels, covering 72 page-states including interactive states (an expanded issue row, a switched tab, an open share popover, an open download dialog). Result on 8 September 2026: 72 page-states checked, no violations, 14 designed states the run could not reach. No screen reader pass and no manual keyboard-only traversal has been done, so the claim rests on automated evidence plus source review only (gaps ACC-GAP-01 and ACC-GAP-02). | |
| app.latchkey.app portal: projects, scans and metrics | WCAG 2.2 Level AA | Not Evaluated | No evaluation yet | Nothing has been run against the portal. The self-audit route map in scripts/audit-self.mjs covers the marketing routes, the help centre, the status page, sign-up, log-in and the 404 page. It does not include /projects, /scans or /metrics, so there is no axe result, no keyboard pass and no screen reader pass for this surface. First evaluation is scheduled with the portal work in Phase 3. | |
| help.latchkey.app help centre | WCAG 2.2 Level AA | Partially Supports | 8 September 2026 | Served from the same Next.js application and the same marketing layout as the site above, and audited in the same run: axe-core 4.10 at 1440, 1200, 375 and 320 CSS pixels with no violations on 8 September 2026. It inherits the shared footer, so it inherits issue ACC-003. No screen reader pass and no manual keyboard-only traversal (ACC-GAP-01, ACC-GAP-02). | |
| status.latchkey.app status page | WCAG 2.2 Level AA | Partially Supports | 8 September 2026 | Audited in the same axe-core 4.10 run at 1440, 1200, 375 and 320 CSS pixels with no violations on 8 September 2026. The audit sees the page as rendered at that moment: an incident state, a degraded state and a maintenance state were not present during the run and so were not evaluated. It inherits the shared footer and issue ACC-003. No screen reader pass and no manual keyboard-only traversal (ACC-GAP-01, ACC-GAP-02). | |
| The widget's own visitor interface, as shipped, in a Shadow DOM host on a fixture site | WCAG 2.2 Level AA | Not Evaluated | No evaluation yet | The widget browser corpus (packages/latchkey-widget/test/browser, 39 cases across 6 files) loads the shipped loader and interface bundles in a real browser on a fixture page and asserts mounting into a Shadow DOM host, initialisation, lifecycle, behaviour under a strict Content Security Policy, isolation from host CSS, and locale resolution. That is behavioural evidence, not a conformance evaluation: the corpus runs no axe-core pass over the interface and no screen reader has ever been pointed at it. Two defects listed below were found by reading the source rather than by evaluating the surface, which is why they appear here under a claim of Not Evaluated. First evaluation is scheduled for Phase 2 with the axe pass over the mounted interface. | |
| Published accessibility statement pages at /statement/{publicId} | WCAG 2.2 Level AA | Not Evaluated | No evaluation yet | The route is not built yet. A request to /statement/{publicId} on the current build returns 404, so there is no surface to evaluate and no claim is being made. The statement generator and its public pages land in Phase 2, and the first evaluation is scheduled with them. | None recorded |
| Public Evidence Pack share view at /share/evidence-pack/{token} | WCAG 2.2 Level AA | Not Evaluated | No evaluation yet | The route is not built yet. Sharing lands in Phase 5 module 10, and the surface is registered here before it exists so that the first evaluation is a precondition of shipping it rather than a follow-up. When it does ship it is audited in both themes and gets a manual pass, not only axe: the passcode gate, the access-denied states and the read-only pack render are each their own page-state. | None recorded |
| PDF deliverables: the Remediation Report and the Evidence Pack | PDF/UA-1 with WCAG 2.2 Level AA Level AA | Not Evaluated | No evaluation yet | A tagged PDF is a different conformance question from a web page and needs its own tool. The only print surface that exists today is /scan/{scanId}/print, rendered by the (print) route group as HTML for the browser to print. It is not in the self-audit route map, no tagged-PDF or PDF/UA check has been run against any generated file, and this repository contains no such checker. Choosing one and running it is Phase 2 work owned by reports-eng. | None recorded |
What each claim means
The five values are the reporting vocabulary a procurement reviewer already knows. Not Evaluated is one of them, and it is used here rather than leaving a blank or borrowing another surface’s claim.
- Supports
- Every part of the surface we evaluated meets the criterion.
- Partially Supports
- Some functionality on the surface does not meet the criterion.
- Does Not Support
- The majority of the functionality on the surface does not meet the criterion.
- Not Applicable
- The criterion is not relevant to this surface.
- Not Evaluated
- The surface has not been evaluated against the criterion. No claim is being made.
Out of scope, and why
Three surfaces a reader could reasonably expect to find above are not ours to claim for. A vendor that quietly includes a checkout it does not control in its own conformance claim is making a claim it cannot support, and a vendor that quietly drops the line saying so leaves a reader to assume it does control it.
- Dodo Payments' hosted checkout and its customer portal
- Payment is taken on a page Dodo Payments serves and controls. We do not write its markup, we cannot fix a barrier in it, and we would be making a claim we cannot support if we counted it as ours. We link their own statement instead, and a barrier reported to us on that page is forwarded with a copy to the reporter. Their own accessibility statement.
- Embedded platform admin frames (Shopify, Wix, Webflow and other marketplace hosts)
- Our app renders inside chrome the platform owns: their navigation, their modals, their focus management, their colour tokens. We claim the interface we render inside that frame and nothing around it, because the surrounding controls are not ours to evaluate or to fix.
- Customer websites the widget runs on
- A customer site is the thing our product works on, not a surface we can claim for. Its markup is the customer's, the widget adjusts a defined set of properties on it, and the coverage table on each result says what was and was not addressed. Reporting a customer site as conforming because our widget is installed on it is the overlay claim this company exists to argue against.
The honest half
What we have not evaluated, and what not to read into the claims
No screen reader has been run against any Latchkey surface. NVDA, JAWS, VoiceOver and TalkBack passes have not been done, and this report records that as an evaluation gap with an owner and a target phase rather than as a pass. Where a surface has had nothing run against it at all, its claim above reads Not Evaluated. Each gap below carries the sentence that matters most: what a reader must not conclude from the table above.
Known issues
Our own barriers, with the WCAG criterion each maps to, who it affects, whether there is anything you can do about it today, who owns it and when we intend to fix it. Every row is generated from the same registry our own accessibility work is tracked in, so the public list cannot be a shorter version of the internal one. Each row has a permanent link, so a report we answer can point at the exact issue.
- Open (4)
- In progress (0)
- Fixed (3)
| # Issue reference | What is wrong | Surface | WCAG criterion | Who it affects | Workaround available now | Owner | Target | Status |
|---|---|---|---|---|---|---|---|---|
| ACC-001 | The widget panel never declares the language of its own content. The panel sets its text direction from the active translator on every render, but no code anywhere in the widget sets a lang attribute, so a panel rendered in Japanese inside an English host page is announced with the English voice and English pronunciation rules. | The widget's own visitor interface | 3.1.2 Language of Parts (AA) | Screen reader users on any host page whose language differs from the interface language they chose, which is the normal case for a visitor who switches the panel to their own language on an English site. | None inside the widget. A site owner whose visitors mostly share one language can set the widget default language to match the host page lang, so the two at least agree. | widget-eng | Phase 2 | Open |
| ACC-002 | Two of the 35 interface languages the widget offers are not translated. The Vietnamese and Simplified Chinese locale files ship English strings, so a visitor who selects either gets an English interface from a menu that offered them their own language. | The widget's own visitor interface | 3.1.2 Language of Parts (AA) | Vietnamese and Simplified Chinese speaking visitors, who are the people least able to use the fallback they are given. | The other 33 languages are translated, and English is served rather than blank strings, so the interface stays operable for anyone who reads some English. | widget-eng | Phase 2 | Open |
| ACC-003 | Four links in the site footer point at pages that do not exist. Privacy, Cookies and local storage, Billing terms and Evidence Pack policy all return 404, so a reader following the footer to a policy page lands on an error. | latchkey.app marketing site, help.latchkey.app help centre, status.latchkey.app status page | 2.4.4 Link Purpose (In Context) (A) | Everyone, and most sharply anyone who relies on the footer as the predictable route to policy pages rather than on search, including screen reader users navigating by landmark and link list. | None. There is nothing a reader can do about this one until we fix it. | legal-ops | Phase 2 | Open |
| ACC-004 | The share control on a scan result computes whether the result has expired once, while rendering. If a result expires while the page is open, the button keeps reporting itself as available: aria-disabled stays false and the tooltip that says the result has expired never appears. React's own purity rule flags the call as an error. | latchkey.app marketing site | 4.1.2 Name, Role, Value (A) | Anyone who leaves a result page open past its retention window, and screen reader users in particular, because the disabled state is the only signal the control gives them that the link will not work. | Reloading the page recomputes the state. Nothing on the page tells you to. | growth-eng | Phase 2 | Open |
| ACC-005 | Every primary button rendered dark ink on brand green at 2.6:1, on the home page of an accessibility product. The cause was a utility name collision: the type scale was implemented as text-body-lg, text-h1 and so on, and tailwind-merge cannot tell a custom text-* utility from a text colour, so it treated them as one group and silently dropped the colour. The scale is now type-*, which cannot collide by construction. | latchkey.app marketing site, app.latchkey.app portal | 1.4.3 Contrast (Minimum) (AA) | Anyone with low vision or reading in bright light, on every primary action on the site. It was invisible in development because the scaffold forced dark mode and dark ink on a dark ground happened to pass. | None. There is nothing a reader can do about this one until we fix it. | design-system | Phase 0 | Fixed |
| ACC-006 | Every control was 32 pixels tall, below both WCAG 2.5.8 and the design system's own 44 pixel floor, and navigation links sized themselves to their text. Fixed in the Button component rather than at each call site, because per-caller means the next caller gets it wrong. | latchkey.app marketing site, app.latchkey.app portal | 2.5.8 Target Size (Minimum) (AA) | Anyone with a tremor, limited fine motor control, or a touch screen, on every button and navigation link in the product. | None. There is nothing a reader can do about this one until we fix it. | design-system | Phase 0 | Fixed |
| ACC-007 | The 404 page had no lang attribute and no title, because notFound() called from inside a page makes Next render its own error document instead of the root layout. The missing-result cases now redirect to a path that deliberately has no route, so Next renders the real root boundary and the page gets its language and its title back. | latchkey.app marketing site | 3.1.1 Language of Page (A) | Screen reader users reaching a missing or expired result, who got the page read in whatever language their reader defaulted to, with no title to tell them where they were. | None. There is nothing a reader can do about this one until we fix it. | platform-eng | Phase 0 | Fixed |
Where a row is confirmed, the registry also records the command, file or commit that confirms it, so a row nobody can trace back to evidence fails our build rather than padding this list.
No row is withheld from this table. A row can be withheld only with a written reason, which appears here in place of the row, and a withheld row with no reason fails the build. That friction is the control: shortening this list leaves a trace in review.
How this site is checked before it ships
The gate is a script in our repository: scripts/audit-self.mjs. It launches a headless Chromium, opens each page of this site that it knows about, injects axe-core 4.10 or later (the same engine every finding in our product comes from) and asks it for the five rule tags the script names: WCAG 2.0 and 2.1 at Level A and AA, plus the Level AA additions in 2.2. A violation makes the run exit non-zero, which is what stops the release: this is a gate rather than a report someone reads later.
Every page is checked at four widths: 1440, 1200, 375 and 320 CSS pixels. Neither end of that range is decoration. WCAG 1.4.10 states reflow at 320 pixels, and 320 is where a sticky bar, a wide table or a fixed-width code block starts covering something. 375 alone never catches it. At the other end, the header’s dropdown menu only renders from 1280 up, so a list that stopped at 1200 would audit the mobile sheet at every width and never once load the menu it replaces.
The phrase that does the work is “in every designed state”. A page can pass empty and fail with content, or pass collapsed and fail expanded, so states are checked separately. When the harness cannot reach one, it prints it in a “states not audited” block and the summary line says how many were missed. A gate that printed a green line for a screen it never loaded would be worse than no gate at all. The states it cannot reach today are recorded above as gap ACC-GAP-03, because the result screens are the most consequential states on this site and they are the unaudited ones.
A second script, scripts/lint-conformance.mjs, validates the report you are reading. It fails the build when the evaluation is more than 120 days old, when a surface has no claim, when a claim uses a word outside the five values, when a claim of Supports has no assistive technology pass behind it, when a known issue cites a criterion that is not in WCAG 2.2, and when a row is withheld from the table above without a written reason.
What the gate covers today
The result screens each need a scan already in that state, supplied as a fixture id on the command line. Run without fixtures (which is the usual local run) and those states are reported as skipped, never as passing.
| Page or state | In the gate today | What that means |
|---|---|---|
| Marketing home | Audited | / covers the hero, the scan form, the dark band and the footer, at every width the run uses. |
| Free scan landing | Audited | /scan as a visitor first sees it. |
| Scan landing, prefilled | Audited | An address arriving in the query string, which renders a different form state. |
| Scan landing, inline validation error | Audited | Reached by submitting an invalid address. Client-side validation only, so it needs no fixture and burns no scan quota. |
| Navigation, mega-menu open | Audited from 1280 up | The header dropdown is a designed state of its own: its links, its column headings and its promo panel are not in the page at all until a trigger is pressed. Below 1280 that component is not rendered, so the run reports this state as skipped rather than as passing. |
| Navigation, mobile sheet open | Audited below 1280 | The same navigation as a sheet over the page, with one group expanded so the accordion’s open state is audited too. From 1280 up the hamburger is not rendered and this one reports as skipped. |
| Limitations | Audited | /limitations, the page that carries the coverage argument. |
| The rest of the marketing pages | Audited | /widget, /flow, /services, /pricing, /compliance, /accessibility, /about, /contact, /help, /status and /signup: every page the header and the footer name, at the same four widths. |
| Log in | Audited | /login, including its form labelling. |
| Not-found page | Audited | An unmatched scan path. The run also asserts the response really is a 404, because the shell Next renders for a page-level notFound() has no lang attribute and passed nothing. |
| Result screen: barriers found | Skipped without a fixture | Needs a completed scan with barriers. Seven states are checked from that one fixture, including a row expanded, each results tab, and the share and download dialogs. |
| Result screen: no failures with checks to review | Skipped without a fixture | Needs a completed scan with no violations and at least one check that needs a person. |
| Result screen: no automated failures | Skipped without a fixture | Needs a completed scan with no violations and nothing to review. |
| Result screen: scan failed | Skipped without a fixture | Needs a scan row in the failed state. |
| Result screen: result expired | Skipped without a fixture | Needs a scan past its expiry, or one whose screenshot has been purged. |
| Result screen: scan in progress | Skipped without a fixture | Needs a scan still queued or running. |
Evidence, not a promise
What our own gate caught on our own pages
ACC-005 · Fixed 7 September 2026
1.4.3 Contrast (Minimum) (AA)
ACC-006 · Fixed 7 September 2026
2.5.8 Target Size (Minimum) (AA)
ACC-007 · Fixed 7 September 2026
3.1.1 Language of Page (A)
None of these were found by a reviewer being generous. Each one was written, pushed, and stopped by the run, which is the argument for having a gate at all, and the reason this section is longer than the one about our commitment. They stay on the known-issues table above with the date they were fixed.
Limits of the method itself
Separate from the issues above and from the gaps: these are limits of how we evaluate, not defects and not surfaces. Written the way we would want a vendor to write it for us. If one of these matters to you and is not moving, say so. A limitation that has been listed for a year without being worked on is a different problem from a limitation.
The same third applies to us
Automated checks cover roughly one third of WCAG success criteria, and this site is checked by the same automation we sell. So a run with nothing to report means the automated third found nothing on the states it reached. It says nothing about whether our reading order matches our visual order, whether our alt text is any good, or whether a form error makes sense read aloud out of context.
The run collects violations, not the maybes
The gate asks axe for violations only. Checks axe cannot decide on its own (the ones our own product reports as needing review) are not collected, so they are not counted here either way. That is a deliberate scope for a build gate and a real gap in this document.
The design system is light-only, on purpose
No dark palette has been designed. The contrast figures our palette is built on (brand indigo on white, danger red on its own tint) are properties of the light palette and do not transfer, and an invented dark palette would ship contrast failures on a product that sells accessibility. If you browse with a dark-mode preference, this site stays light.
A page not on the gate’s list is not audited by it
The gate opens the pages it has been given and discovers nothing on its own. Every page of this site we have actually published is on that list today, but a page added tomorrow is not covered until someone adds it there too, and neither is a state nobody wrote a step for. The printable version of a result is one we have not added. We would rather name the gap than describe the gate as if it found pages by itself.
We are in private beta, and some pages are still being written
A link in the navigation can still reach a page that is not published yet. That is a broken journey for anyone, and a worse one for someone using a screen reader who has to work out whether the mistake was theirs. Four such links are on the known-issues list above as ACC-003.
Report a barrier on this site
This document
- Version and dates
- Version 1, evaluated 8 September 2026, next evaluation due 7 December 2026 on a 90 day cadence. Version 1 is the first dated report rather than the fifth. When it is re-evaluated the version goes up and the dates move with it. There is no version archive at its own address yet, so an older version of this report is not linkable today.
- Owner
- accessibility-lead. Barrier reports go to accessibility@latchkey.app.
- Both names resolve
- /accessibility-statement is a permanent redirect (HTTP 308) to this page, because both names are used in our own plan and in the site footer, and a link from either should reach the report rather than a 404.
- How the freshness notice works
- This page is statically generated when the site is built, so the notice above reflects the report’s age at build time rather than at the moment you load it. The backstop is the build itself: it fails once the evaluation is more than 120 days old, which is 30 days after the cadence date, so a deploy cannot carry a report that has gone stale past that window.