Inspiration
When we think about accessibility in software, we usually think about the color contrast, button sizes, etc. on the UI interface. But accessibility is not just a UI problem—what about documentation? Since it is not present to the actual user, most people will not think about that. Documentation is often the first thing a developer or contributor interacts with, but it is usually overlooked because it is not part of the visible interface. For people who rely on screen readers, such as people who have low vision, blindness, or even cognitive disabilities, it will be a barrier for them if there is missing alt text or a broken heading hierarchy. It is not because the developers don't care; it's because they are not aware, and no one reminds them of that. These issues slip through code review, get merged, and get deployed. During all these processes, it excludes a whole group of developers implicitly. We built a A11y GitLab CI Component for those invisible developers. The idea is very simple: if we can lint code, we can also lint accessibility. By integrating accessibility checks directly into CI, we need to catch it at the source, before it ever reaches production, before it ever becomes someone's barrier.
What it does
Before A11y GitLab CI Component, there was a lack of checks for accessibility issues in documentation. It is hard to find out there is a missing alt text, an empty link, or a broken heading hierarchy during code review, get merged, and get deployed. A good way to catch it was by manually checking it, which does not often happen in the real world. A11y GitLab CI Component adds an automated accessibility gate directly into the GitLab CI/CD pipeline. When the developer pushes the code, it will automatically scan all the Markdown files from the repository and check if there are any violations that are against WCAG 2.1 standards. If there are any, the pipeline blocks the merge and generates a visual HTML report pointing out which file, which line, and which WCAG principle was violated. It requires no configuration to get started. Any GitLab project can integrate it with two lines of YAML, and it will automatically scan the entire repository on every push.
How we built it
We built A11y GitLab CI Component in three layers. The first layer is the scanning engine. We used markdownlint-cli2 as the core scanner; it is configured through markdownlint-cli2.jsonc to enable all default rules but also disable some irrelevant rules. We also added a JSON formatter so that the results can be written to a structured a11y-report.json file, which is dynamically generated during the pipeline runs. The second layer is the report generator. We wrote a Node.js script called a11y-to-html.js that reads the generated a11y-report.json output and converts it into a visual HTML report. In the report, each violation is mapped to its corresponding WCAG 2.1 success criterion, so developers know where it is wrong immediately. The third layer is the CI gate. We put everything into a reusable GitLab CI Component that is defined in templates/md-check.yml. After the scan and report generation, the guard script counts how many violations there are. If the count is greater than zero, then it exits with code 1 and stops the merge process. The HTML report will always be generated and saved as a pipeline artifact no matter if it passes or fails, so developers can always download and review it.
Challenges we ran into
The challenge we ran into was testing it instead of building the tool. Our component's purpose is to block pipelines when accessibility violations exist. But our own test suite intentionally contains files with violations to verify the tool works. This created a paradox where our own CI was always red or failed, even when everything was working perfectly. Simply ignoring failures was not a good option. A broken tool that silently detects nothing would also show green, which gives us false confidence. So we needed a way to distinguish between "the tool is working and found violations" and "the tool is broken and found nothing." To solve this issue, we introduced a test_mode input into the component template. When it is enabled, the guard logic inverts: finding violations means the test passes, and finding nothing means something is broken. However, real failures such as a missing dependency or a report that was never generated still cause the pipeline to fail naturally, because those steps never reach the guard logic at all. The end users will never see test_mode. The default is false, so the component behaves exactly as expected in any real project. If violations are found, the merge is blocked. If the documentation is clean, it passes through.
Accomplishments that we're proud of
The accomplishment that we’re proud of is that every time a new commit is pushed, the pipeline automatically runs and checks all the Markdown files for accessibility, which helps people who rely on screen readers to browse documentation without any barriers. We are also proud of the HTML report; it does not just list out the errors. Each violation is mapped to its corresponding WCAG 2.1 success criterion and also includes suggestions from AI on how to revise it. Therefore, developers know what is wrong and how to correct it immediately.
What we learned
Before this project, we never thought about accessibility in documentation. Since most of the time, we think that accessibility is usually associated with UI elements such as color contrast and button sizes, etc. However, documentation also has its own set of accessibility requirements that are also important and cannot be overlooked. If there is missing alt text or a broken heading hierarchy, it can make the page inaccessible to people who rely on screen readers.
What's next for Accessibility (A11y) Agentic CI Component
Currently, A11y GitLab CI Component only scans and checks all the Markdown files. The next step for our project is to extend the tool to support HTML files, which allows teams that write documentation directly in HTML to benefit from the same automated accessibility checks.
Log in or sign up for Devpost to join the conversation.