Skip to content

Repository files navigation

Touchstone

Touchstone

Automated WCAG accessibility checks for Jetpack Compose — catch violations in CI before your users do.

CI Maven Central - lint Maven Central - compose License


The problem

Accessibility is hard!!!

What it does

  • Dual-engine — Static lint rules (no emulator) + runtime test assertions (rendered UI)
  • Compose-first — Built specifically for Jetpack Compose, not retrofitted from the View system
  • Zero config — One Gradle line per engine. No annotation processors, no setup
  • WCAG-mapped — Every rule links to its WCAG 2.1 success criterion
  • Low noise — 5 touch-target exemptions, 1% pixel noise filter, and Compose API guards prevent false positives
  • Tested — 30+ tests across both engines. Contrast pipeline uses CIE-94 color distance (same approach as Google's ATF internally)

Quick Start

Engine 1: Lint Rules (static analysis, no emulator)

dependencies {
    lintChecks("io.github.lehan0328:touchstone-lint:0.1.0")
}
./gradlew lint

That's it. Touchstone will flag accessibility violations alongside your existing lint checks:

ProfileScreen.kt:21: Error: Image is missing required contentDescription
parameter. Pass a descriptive string for screen readers, or null if
decorative. [TouchstoneMissingContentDescription]
    Image(painter = painterResource(R.drawable.avatar))
    ~~~~~

ProfileScreen.kt:35: Warning: fontSize uses .dp which won't scale with
user font size preference. Use .sp instead. [TouchstoneHardcodedFontSize]
    Text("Hello", fontSize = 14.dp)
                              ~~~~~

Engine 2: Test Assertions (runtime analysis, requires emulator)

dependencies {
    androidTestImplementation("io.github.lehan0328:touchstone-compose:0.1.0")
}

Add two lines to any Compose test:

@get:Rule val composeTestRule = createComposeRule()
@get:Rule val a11yCheck = A11yCheckRule()

@Test
fun myScreen_isAccessible() {
    composeTestRule.setContent { MyScreen() }
    val report = a11yCheck.check(composeTestRule)
    assert(report.passed) { report.format() }
}

Sample output (from a real run against a test app with intentional violations):

Touchstone Accessibility Report
═══════════════════════════════
Score: 57/100
Violations: 5 (4 critical, 1 warning)

1. [CRITICAL] Touch target is 24.0×24.0dp (minimum: 48×48dp)
   Rule: touch-target-size (WCAG 2.5.5 AA)
   Fix: Add Modifier.size(48.dp) or Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)

2. [CRITICAL] Contrast ratio is 1.61:1 for normal text (minimum: 4.5:1)
   Rule: color-contrast (WCAG 1.4.3 AA)
   Fix: Increase contrast between text and background colors

3. [WARNING] Editable field has no accessible label
   Rule: editable-text-semantics (WCAG 4.1.2 A)
   Fix: Add a label parameter to TextField, or use Modifier.semantics { contentDescription = "..." }

Rules

Lint Rules (./gradlew lint)

Rule Severity WCAG What it catches
TouchstoneMissingContentDescription Error 1.3.1, 4.1.2 Image() or Icon() without contentDescription
TouchstoneHardcodedFontSize Warning 1.4.4 fontSize = 14.dp instead of 14.sp
TouchstoneClickableWithoutRole Warning 4.1.2 Modifier.clickable {} without role parameter

Test Rules (./gradlew connectedAndroidTest)

Rule Severity WCAG What it catches
touch-target-size Critical 2.5.5 Interactive elements < 48×48dp (with 5 exemptions)
color-contrast Critical 1.4.3 Text contrast below 4.5:1 (normal) or 3.0:1 (large)
editable-text-semantics Warning 4.1.2 Text fields missing editableText, ImeAction, or label
duplicate-clickable-bounds Warning 2.5.5 Overlapping clickable regions (≥ 10%)

How It Works

Touchstone uses a dual-engine architecture:

Lint Engine TestRule Engine
Runs during ./gradlew lint ./gradlew connectedAndroidTest
Analysis type Static (code patterns) Runtime (rendered properties)
Catches Missing labels, hardcoded sizes, missing roles Touch target sizes, contrast ratios, duplicate bounds
Requires emulator No Yes
Speed < 1 second 50–200ms per screen

Requirements

  • Android Gradle Plugin 7.0+
  • Kotlin 1.7+
  • touchstone-lint: No emulator required
  • touchstone-compose: Requires emulator or device for instrumented tests

Roadmap

  • Android Lint rules (static analysis)
  • Compose TestRule engine (runtime — touch targets, contrast, semantics)
  • GitHub Action + PR commenting

Contributing

See CONTRIBUTING.md for development setup, how to add new detectors, and coding standards.

# Lint (unit tests, no emulator)
./gradlew :touchstone-lint:test

# Compose (unit tests, no emulator)
./gradlew :touchstone-compose:testDebugUnitTest

# Compose (instrumented tests, requires emulator)
./gradlew :touchstone-compose:connectedDebugAndroidTest

License

Copyright 2026 Lehan Ouyang

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

If you found this tool useful, a ⭐ on the repo would be much appreciated!

About

No description, website, or topics provided.

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages