What it does today
- Adds one Gradle task,
accessStamp, to an Android module. - Scans XML layouts and Jetpack Compose source for a small set of patterns: image labels, input labels, small touch targets, text size in dp, and declared color contrast.
- Writes an HTML report and a JSON report, with stable finding IDs, before the check fails the build.
- Fails only on new unreviewed candidates once you have a baseline.
- Can run your existing instrumentation task and read its fresh JUnit results as a separate runtime check.
What it does not do
- It does not resolve styles, themes, includes, Java source, generated code or merged variant resources.
- It does not crawl screens. Runtime checks cover only the screens your own tests visit.
- Runtime checks have compiled but have not been run on a device or emulator. No runtime findings are claimed. You need an API 35 device or emulator to try them.
- It does not upload your source or reports anywhere.
Requirements
- JDK 17 and an internet connection for Gradle and Maven dependencies.
- For the Android fixture only: Android SDK platform 35, build tools 34.0.0, and
ANDROID_HOMEor alocal.propertiesfile. The plugin tests and the SDK-freescan-fixtureneed no SDK.
Step 1: Get the source and build it
cd accessstamp
./gradlew :plugin:test :plugin:jar
./gradlew -p scan-fixture accessStamp
The second command runs the plugin on a small fixture with eight intentionally broken elements. You should see a report in scan-fixture/build/reports/accessstamp/.
Step 2: Include the plugin build in your app
Place the plugin directory outside your app build, for example as a sibling folder named accessstamp. In your app's settings.gradle.kts:
pluginManagement {
includeBuild("../accessstamp")
repositories { google(); mavenCentral(); gradlePluginPortal() }
}
Step 3: Apply and configure
In the Android app module's build.gradle.kts:
plugins {
id("com.android.application")
id("dev.accessstamp")
}
accessStamp {
appId = "com.example.app"
appVersion = "1.2.3"
variant = "debug"
journeys = listOf("Sign in", "Checkout")
notTested = listOf("Password reset", "Screen reader manual review")
}
Values such as variant, the build identity and journey names are metadata you supply. They are recorded in the report as given, not checked. Journey names are an inventory, never proof that a journey was tested.
Step 4: Run it and read the report
./gradlew :app:accessStamp --continue
Open app/build/reports/accessstamp/report.html. The JSON file next to it is for tooling. Use --continue in CI so the report is written even when the task fails. The report lists the scanned source hash, the findings, and a coverage ledger of what was and was not tested.
Step 5: Review and baseline
Fix or review each candidate. If you accept existing debt, record why:
./gradlew :app:accessStampBaseline -PbaselineReason="Reviewed legacy debt, tracked in A11Y-123"
This replaces the baseline with the current findings. It never runs automatically. Review the diff and commit it. Blank reasons and malformed files fail the build. After that, accessStamp fails only on new candidates. Do not baseline without reading what you are accepting.
Optional: runtime checks
If your module already has instrumentation tests, point the plugin at the task:
accessStamp {
runtimeTasks = listOf("connectedDebugAndroidTest")
}
Your tests must enable the accessibility checks themselves, with Espresso AccessibilityChecks.enable() for Views or enableAccessibilityChecks() for Compose. A passing JUnit result shows that tests ran, not that checks were enabled. Compose checks need API 34 or higher. See the fixture's androidTest folder for compiled examples.
Known limits
- Finding IDs can change when elements are moved or renamed, or when Compose calls are inserted. Review baseline diffs.
- The 48dp touch target rule is Android guidance, not a WCAG measurement.
- No coverage figure is reported, by design.