Catches Jetpack Compose accessibility issues in your code at build time — before you ever open the app.
ComposeA11yLint is a free, open-source detekt ruleset that statically analyzes Jetpack Compose / Kotlin source for accessibility problems and maps each finding to a WCAG success criterion. No device, no emulator, no runtime — issues surface in code review and CI. Your source never leaves your machine; zero network calls.
| Runtime tools (Accessibility Scanner, on-device TalkBack testing) | ComposeA11yLint | |
|---|---|---|
| Stage | Runtime — running app, manual testing | Build time — source code, automatic |
| Mechanism | ADB / on-device overlays | Static analysis of Compose source (AST) |
| You learn of an issue | After build + deploy + manual navigation | In code review / on every commit |
| Needs a device | Yes | No |
It's complementary to runtime tools: ComposeA11yLint flags issues in code; on-device tools help verify the fixes.
ComposeA11yLint is a detekt ruleset — it runs inside detekt, so you need
the detekt Gradle plugin applied. It targets detekt 2.0 (plugin id dev.detekt); a detekt 1.x
setup (io.gitlab.arturbosch.detekt) can't load it.
Adding the ruleset is additive — it does not change any rules or reports you already run. In the
build.gradle.kts of the module you want analyzed (for an Android app, the app module):
plugins {
// ...your existing plugins...
id("dev.detekt") version "2.0.0-alpha.3" // only if you don't already apply detekt
}
repositories {
mavenCentral() // where the ruleset is published
}
dependencies {
detektPlugins("io.github.pardip235:composea11ylint:0.1.0")
}The rules are off by default (as all detekt custom rules are) — turn them on in your detekt
config. If you already have a config/detekt.yml, just add this block to it; otherwise create the
file:
compose-a11y:
MissingContentDescription:
active: true
TouchTargetSize:
active: true
ClickableWithoutRole:
active: true
CustomComponentMissingSemantics:
active: true
VisualHeadingWithoutHeading:
active: true
ColorContrast:
active: truePoint detekt at your config (skip if it's already configured):
detekt {
buildUponDefaultConfig = true // keep detekt's defaults; add ours on top
config.setFrom("$rootDir/config/detekt.yml") // can list several files
}./gradlew detektThe ComposeA11yLint rules now run alongside whatever else your detekt setup already does. (On
Android you can also use detektMain; the plain detekt task analyzes src/main/kotlin regardless
of your AGP version.)
| Rule | Flags | WCAG |
|---|---|---|
| MissingContentDescription | Image/Icon with empty or junk contentDescription |
1.1.1 |
| TouchTargetSize | Clickable smaller than 48dp | 2.5.8 |
| ClickableWithoutRole | Modifier.clickable {} with no role/label |
4.1.2 |
| CustomComponentMissingSemantics | Custom interactive composable, no semantics | 4.1.2 |
| VisualHeadingWithoutHeading | Heading-styled Text without heading() |
1.3.1 |
| ColorContrast | Literal foreground/background colors below the 4.5:1 ratio | 1.4.3 |
Precision first — each rule fires only when the violation is statically determinable, so a few rock-solid rules beat many that misfire.
Every detekt run prints a one-line console summary from ComposeA11yLint. To also write the
ruleset's own report files, map the report ids to output paths on the detekt tasks. This is
additive — it leaves detekt's built-in reports untouched:
tasks.withType<dev.detekt.gradle.Detekt>().configureEach {
reports {
custom {
reportId = "composeA11yJson" // stable JSON schema
outputLocation.set(file("build/reports/detekt/composea11y.json"))
}
custom {
reportId = "composeA11yMarkdown" // grouped-by-file Markdown
outputLocation.set(file("build/reports/detekt/composea11y.md"))
}
}
}| Output | Where to find it | What it is |
|---|---|---|
| Console summary | stdout of the detekt task |
ComposeA11yLint: N issues across M files (h high, m medium, l low) |
composeA11yJson |
build/reports/detekt/composea11y.json |
Stable machine-readable schema — for CI, dashboards, other tools. |
composeA11yMarkdown |
build/reports/detekt/composea11y.md |
Human-readable, grouped by file, with WCAG + detail columns. |
Paths are relative to the module you configured (for the app module → app/build/reports/detekt/).
The JSON schema is documented in CONTRIBUTING.md.
If you want a run that reports only ComposeA11yLint findings — nothing from detekt's built-in
rules — set disableDefaultRuleSets = true and (optionally) silence the built-in report formats.
This changes detekt's behavior for that module, so prefer a dedicated setup over a shared one:
detekt {
disableDefaultRuleSets = true // load ONLY rulesets from detektPlugins (this one)
buildUponDefaultConfig = true
config.setFrom("$rootDir/config/detekt.yml")
}
tasks.withType<dev.detekt.gradle.Detekt>().configureEach {
reports {
// ...the two custom { } blocks from above...
checkstyle.required.set(false)
html.required.set(false)
sarif.required.set(false)
markdown.required.set(false)
}
}Published to Maven Central as io.github.pardip235:composea11ylint. All six rules and the
JSON / Markdown / console reports are implemented with unit tests.
./gradlew :rules:testThe :rules module is a pure Kotlin/JVM detekt ruleset (no Android dependencies).
Looking for compliance reporting? A separate companion project builds reports from this ruleset's output — see composea11ylint-conformance.
Apache-2.0.