What if GraphQL Knew
Accessibility?
Dynamic Accessibility Summaries with Directives
apidays Paris 2025 December 911, 2025
Vanessa Johnson
Impact
Why This Work
Vanessa Johnson
Android Engineer
The New York Times
Make accessibility a first class citizen in API design.
Can work across Android, iOS, and Web teams.
Believe that programmatic accessibility metadata
can bridge the gap between API design and
inclusive user experiences, making the user
experience better for everyone.
About Me
Why Accessibility Matters
The invisible user base we can't afford to ignore
1.3B
People with
disabilities globally
16% of world
population
71%
Leave inaccessible
sites immediately
Lost customers due
to poor UX
3%
Of the web is
fully accessible
WebAIM Million 2025
report
The Accessibility Gap in APIs
How APIs shape user experience long before UI code is written
APIs Shape UX First
Data structure determines user
flow
APIs define what
information is available and
how it's organized, and
influence UI decisions
before designers even
start.
Accessibility Added
Last
An afterthought in development
Accessibility is typically
addressed during UI
implementation, long after
API contracts are finalized.
Inconsistent
Implementation
No standard approach
Each team implements
accessibility differently,
leading to inconsistent
experiences across
platforms.
APIs Today: What We Get
Standard GraphQL query without accessibility metadata
GraphQL Query
query Movie {
movie(id: "1") {
title
releaseYear
rating
}
}
Pain Points
The challenges of retrofitting accessibility
Leaving components without a11y labels
“Unlabeledˮ
Missing information that will be needed by
screen readers to help users use your product.
Inconsistent Across Platforms
Android ≠ iOS  Web
Each platform team implements accessibility
differently, creating divergent experiences for
the same data.
The Result: Fragile, Inconsistent Accessibility
Every platform reinvents accessibility labels, leading to drift, bugs, and inconsistent screen reader
experiences.
This creates a maintenance nightmare as APIs evolve and teams scale.
The Client Info Flow Problem
Developers handcraft fragile accessibility phrases
Kotlin Example
val summary = "${movie.title},
${movie.releaseYear}, rated
${movie.rating}/10"
String Building is Fragile
Hardcoded templates break when data changes,
localization needed, or edge cases emerge.
Duplicated Logic
Same pattern repeated across Android, iOS,
Web teams with slight variations.
Inconsistent Results
Screen readers hear different summaries for the
same content depending on platform.
The Cost of Doing Nothing
Why accessibility is a business imperative, not just a compliance checkbox
Abandonment
Disabled users leave
difficult sites
immediately
Lost customers &
revenue
Monetary Settlements
Per accessibility lawsuit
Legal risk &
compliance
SEO Traffic Gain
From accessible,
structured content
Discoverability &
reach
The Business Case is Clear
Accessibility isn't just about compliance, it's about market expansion, brand reputation, and
reducing risk.
Companies that invest in accessibility see measurable returns across customer satisfaction,
conversion rates, and operational efficiency.
Initial Schema Without A11y)
Basic Movie type with no accessibility metadata
GraphQL Schema
type Movie {
title: String!
releaseYear: String!
rating: Float!
}
Core Idea: Embed Accessibility
Metadata
query {
title
releaseYear
rating
a11y // example
}
Shifting Left: The Vision
Make accessibility part of the API contract, not a frontend afterthought
Encode Semantics
Embed accessibility metadata
directly in GraphQL schema using
custom directives
Generate Types
Create strongly typed models
that carry a11y metadata to clients
Deliver Defaults
Provide accessible labels, tokens,
and templates to all platforms
from day one
"Accessibility should be baked into the API, not sprinkled on top of the UI later."
Schema Wiring
Inject a11y
Add per field hints
Schema +
Directives
Enhanced
Schema
Response
summary: “The
movie
Inception,
released on
2010-07-16, is
rated 8.8 out of
10."
Custom
Wiring
Plugin
Core Idea: Embed Accessibility Metadata
Use GraphQL directives to annotate types and fields with accessibility information
@a11yLabel
Declare which field is the main
label for screen readers
@a11yToken
Structured data type with priority,
units, and formatting
@a11yTemplate
Sentence template with
placeholders for consistent
phrasing
The Goal: Consistent, Accessible Experiences
Clients assemble labels, tokens, and templates to generate reliable, localized summaries for screen
readers.
One schema change → consistent accessibility across all platforms.
@a11yLabel Directive
Declare the primary label for screen readers
GraphQL Schema What It Does
Tells screen readers which field contains the
primary, human readable label for this object.
Why It Matters
Provides the main identifier that screen readers
announce first when users encounter this object.
Usage Pattern
Applied to the object type, not individual fields,
points to the field that best represents the object.
directive @a11yLabel(field: String!) on
OBJECT | FIELD_DEFINITION
type Movie @a11yLabel(field: "title") {
title: String!
releaseYear: String!
rating: Float!
}
@a11yToken Directive
Structured data type for screen readers with priority and formatting
GraphQL type + field
The data type and field name this token represents
(e.g., "price", "rating", "date").
priority 0..n
Order of importance for screen readers. Lower
values = higher priority. Default: 0.
unitPrefix / unitSuffix
Format numbers with currency symbols,
percentages, or unit labels (e.g., "$", "/10", "GB").
number: Boolean
Treats the value as a number for proper screen reader
pronunciation (e.g., "299" vs "two nine nine").
directive @a11yToken(
type: String!
field: String!
priority: Int = 0
label: String
labelPosition: String
unitPrefix: String
unitSuffix: String
number: Boolean
) on FIELD_DEFINITION
@a11yTemplate Directive
Sentence template with dynamic placeholders
GraphQL Schema Template Placeholders
Use {fieldName} syntax to reference schema fields
in the template.
Natural Language
Template creates human readable sentences
that screen readers can speak naturally.
Dynamic Assembly
Runtime substitution ensures summaries stay
current with changing data.
directive @a11yTemplate(summary:
String) on OBJECT
type Movie @a11yTemplate(summary:
"{title}, {releaseYear}, rated {rating}/10")
{
title: String!
releaseYear: String!
rating: Float!
}
Complete Schema Example With A11y)
Full implementation with all three directives and generated types
type YourType @a11yTemplate(summary:
"{name}, {price}") {
name: String!
@a11yLabel(field: "name")
@a11yToken(type: "name", field: "name",
priority: 0)
price: Float
@a11yToken(
type: "price",
field: "price",
priority: 1,
unitPrefix: "$",
number: true
)
a11y: A11y!
}
type A11y {
label: String!
tokens: [A11yToken!]
templates: A11yTemplates
}
type A11yToken {
type: String!
value: String!
priority: Int!
}
type A11yTemplates {
summary: String
}
Query Structure
Requesting accessibility metadata alongside regular data
GraphQL Query
Clients request regular data plus a nested
a11y object that contains all accessibility
metadata.
The a11y field provides comprehensive
accessibility information
query Product$id: ID!) {
product(id: $id) {
name
price
a11y {
label
tokens {
type
value
priority
}
templates {
summary
}
}
}
}
Response Structure With A11y)
How clients receive accessibility metadata alongside regular data
JSON Response
{
"product": {
"name": "Noise-Canceling Headphones",
"price": 299.0,
"a11y": {
"label": "Noise-Canceling Headphones",
"tokens": [
{
"type": "name",
"value": "Noise-Canceling Headphones",
"priority": 0
},
{
"type": "price",
"value": "299",
"priority": 1
}
],
"templates": {
"summary": "Noise-Canceling Headphones, $299"
}
}
}
}
Response Structure With A11y)
Label
Main screen reader label
Tokens
Structured data with priority
Templates
Sentence assembly pattern
Android Example Jetpack Compose)
Using generated a11y metadata with Jetpack Compose
Kotlin/Jetpack Compose
Composable
fun ProductCard(product: Product) {
Column(
modifier = Modifier.semantics {
contentDescription =
product.a11y.templates.summary
}
) {
Text(product.name)
Text("$" + product.price)
}
}
Key Features
• Uses generated a11y.templates.summary field
• Direct semantic mapping from API
• Consistent with schema definition
Accessibility Benefits
• Screen readers get meaningful hints
• No manual string building needed
• Platform agnostic implementation
iOS Example SwiftUI
Using accessibility metadata in SwiftUI components
SwiftUI Code
SwiftUI automatically uses the
a11y.templates.summary for Voice
Over announcements.
Provides consistent
accessibility across all
Apple platforms.
struct ProductView: View {
let product: Product
var body: some View {
VStack(alignment: .leading) {
Text(product.name)
.font(.headline)
Text("$(product.price)")
.font(.subheadline)
}
.accessibilityLabel(Text(product.a11y.templates.summary))
}
}
Web Example React
Using a11y metadata in React components with ARIA attributes
React Component
React components can consume a11y
metadata directly through aria-label
attributes.
The a11y.templates.summary
provides a consistent, screen
readable summary for the entire
component.
function ProductCard({ product }) {
const { name, price, a11y } = product;
return (
<div aria-label={a11y.templates.summary}
role="group">
<h3>{name}</h3>
<p${price}</p>
</div>
);
}
Benefits & Impact
What you get when accessibility becomes part of your API contract
Single Source of Truth Removes Brittle Client Logic
Dynamic Per Instance Summaries Optional Field Level Nuance
Result: Consistent, accessible experiences across all platforms with less developer effort
Ownership & Considerations
Key questions and strategies for successful implementation
Who Owns the Directives?
1 Product + Design collaboration
Define semantic meaning and user experience
2 Backend engineers
Implement directives and resolvers
3 Combination
Collaboration of cross-functional teams
Missing Metadata Strategy
Sensible Fallbacks
Use field names as fallback labels when a11y
metadata is missing
Monitoring
Track missing a11y metadata and report gaps
Gradual Adoption
Start with critical fields, expand coverage over
time
Localization Strategy
Template per Locale
Use locale-specific templates in schema
Server-Side Injection
Inject localized strings at runtime
Deferred Loading
Load translations on demand
Versioning & Compatibility
Backward Compatibility
New directives are optional, existing clients
continue to work
Schema Evolution
Add new metadata fields without breaking
changes
Feature Flags
Enable/disable a11y generation per client
Ownership & Considerations
Key questions and strategies for successful implementation
Thank You!
Questions? Let's continue the conversation
github.com/vanessamj99/GraphQL-a11y
LinkedIn
Portfolio
#a11y #APIs
Vanessa Johnson - Android Engineer @ The New York Times
GitHub

apidays Paris 2025 - What If GraphQL Knew Accessibility.pdf

  • 1.
    What if GraphQLKnew Accessibility? Dynamic Accessibility Summaries with Directives apidays Paris 2025 December 911, 2025 Vanessa Johnson
  • 2.
    Impact Why This Work VanessaJohnson Android Engineer The New York Times Make accessibility a first class citizen in API design. Can work across Android, iOS, and Web teams. Believe that programmatic accessibility metadata can bridge the gap between API design and inclusive user experiences, making the user experience better for everyone. About Me
  • 3.
    Why Accessibility Matters Theinvisible user base we can't afford to ignore 1.3B People with disabilities globally 16% of world population 71% Leave inaccessible sites immediately Lost customers due to poor UX 3% Of the web is fully accessible WebAIM Million 2025 report
  • 4.
    The Accessibility Gapin APIs How APIs shape user experience long before UI code is written APIs Shape UX First Data structure determines user flow APIs define what information is available and how it's organized, and influence UI decisions before designers even start. Accessibility Added Last An afterthought in development Accessibility is typically addressed during UI implementation, long after API contracts are finalized. Inconsistent Implementation No standard approach Each team implements accessibility differently, leading to inconsistent experiences across platforms.
  • 5.
    APIs Today: WhatWe Get Standard GraphQL query without accessibility metadata GraphQL Query query Movie { movie(id: "1") { title releaseYear rating } }
  • 6.
    Pain Points The challengesof retrofitting accessibility Leaving components without a11y labels “Unlabeledˮ Missing information that will be needed by screen readers to help users use your product. Inconsistent Across Platforms Android ≠ iOS  Web Each platform team implements accessibility differently, creating divergent experiences for the same data. The Result: Fragile, Inconsistent Accessibility Every platform reinvents accessibility labels, leading to drift, bugs, and inconsistent screen reader experiences. This creates a maintenance nightmare as APIs evolve and teams scale.
  • 7.
    The Client InfoFlow Problem Developers handcraft fragile accessibility phrases Kotlin Example val summary = "${movie.title}, ${movie.releaseYear}, rated ${movie.rating}/10" String Building is Fragile Hardcoded templates break when data changes, localization needed, or edge cases emerge. Duplicated Logic Same pattern repeated across Android, iOS, Web teams with slight variations. Inconsistent Results Screen readers hear different summaries for the same content depending on platform.
  • 8.
    The Cost ofDoing Nothing Why accessibility is a business imperative, not just a compliance checkbox Abandonment Disabled users leave difficult sites immediately Lost customers & revenue Monetary Settlements Per accessibility lawsuit Legal risk & compliance SEO Traffic Gain From accessible, structured content Discoverability & reach The Business Case is Clear Accessibility isn't just about compliance, it's about market expansion, brand reputation, and reducing risk. Companies that invest in accessibility see measurable returns across customer satisfaction, conversion rates, and operational efficiency.
  • 9.
    Initial Schema WithoutA11y) Basic Movie type with no accessibility metadata GraphQL Schema type Movie { title: String! releaseYear: String! rating: Float! }
  • 10.
    Core Idea: EmbedAccessibility Metadata query { title releaseYear rating a11y // example }
  • 11.
    Shifting Left: TheVision Make accessibility part of the API contract, not a frontend afterthought Encode Semantics Embed accessibility metadata directly in GraphQL schema using custom directives Generate Types Create strongly typed models that carry a11y metadata to clients Deliver Defaults Provide accessible labels, tokens, and templates to all platforms from day one "Accessibility should be baked into the API, not sprinkled on top of the UI later."
  • 12.
    Schema Wiring Inject a11y Addper field hints Schema + Directives Enhanced Schema Response summary: “The movie Inception, released on 2010-07-16, is rated 8.8 out of 10." Custom Wiring Plugin
  • 13.
    Core Idea: EmbedAccessibility Metadata Use GraphQL directives to annotate types and fields with accessibility information @a11yLabel Declare which field is the main label for screen readers @a11yToken Structured data type with priority, units, and formatting @a11yTemplate Sentence template with placeholders for consistent phrasing The Goal: Consistent, Accessible Experiences Clients assemble labels, tokens, and templates to generate reliable, localized summaries for screen readers. One schema change → consistent accessibility across all platforms.
  • 14.
    @a11yLabel Directive Declare theprimary label for screen readers GraphQL Schema What It Does Tells screen readers which field contains the primary, human readable label for this object. Why It Matters Provides the main identifier that screen readers announce first when users encounter this object. Usage Pattern Applied to the object type, not individual fields, points to the field that best represents the object. directive @a11yLabel(field: String!) on OBJECT | FIELD_DEFINITION type Movie @a11yLabel(field: "title") { title: String! releaseYear: String! rating: Float! }
  • 15.
    @a11yToken Directive Structured datatype for screen readers with priority and formatting GraphQL type + field The data type and field name this token represents (e.g., "price", "rating", "date"). priority 0..n Order of importance for screen readers. Lower values = higher priority. Default: 0. unitPrefix / unitSuffix Format numbers with currency symbols, percentages, or unit labels (e.g., "$", "/10", "GB"). number: Boolean Treats the value as a number for proper screen reader pronunciation (e.g., "299" vs "two nine nine"). directive @a11yToken( type: String! field: String! priority: Int = 0 label: String labelPosition: String unitPrefix: String unitSuffix: String number: Boolean ) on FIELD_DEFINITION
  • 16.
    @a11yTemplate Directive Sentence templatewith dynamic placeholders GraphQL Schema Template Placeholders Use {fieldName} syntax to reference schema fields in the template. Natural Language Template creates human readable sentences that screen readers can speak naturally. Dynamic Assembly Runtime substitution ensures summaries stay current with changing data. directive @a11yTemplate(summary: String) on OBJECT type Movie @a11yTemplate(summary: "{title}, {releaseYear}, rated {rating}/10") { title: String! releaseYear: String! rating: Float! }
  • 17.
    Complete Schema ExampleWith A11y) Full implementation with all three directives and generated types type YourType @a11yTemplate(summary: "{name}, {price}") { name: String! @a11yLabel(field: "name") @a11yToken(type: "name", field: "name", priority: 0) price: Float @a11yToken( type: "price", field: "price", priority: 1, unitPrefix: "$", number: true ) a11y: A11y! } type A11y { label: String! tokens: [A11yToken!] templates: A11yTemplates } type A11yToken { type: String! value: String! priority: Int! } type A11yTemplates { summary: String }
  • 18.
    Query Structure Requesting accessibilitymetadata alongside regular data GraphQL Query Clients request regular data plus a nested a11y object that contains all accessibility metadata. The a11y field provides comprehensive accessibility information query Product$id: ID!) { product(id: $id) { name price a11y { label tokens { type value priority } templates { summary } } } }
  • 19.
    Response Structure WithA11y) How clients receive accessibility metadata alongside regular data JSON Response { "product": { "name": "Noise-Canceling Headphones", "price": 299.0, "a11y": { "label": "Noise-Canceling Headphones", "tokens": [ { "type": "name", "value": "Noise-Canceling Headphones", "priority": 0 }, { "type": "price", "value": "299", "priority": 1 } ], "templates": { "summary": "Noise-Canceling Headphones, $299" } } } }
  • 20.
    Response Structure WithA11y) Label Main screen reader label Tokens Structured data with priority Templates Sentence assembly pattern
  • 21.
    Android Example JetpackCompose) Using generated a11y metadata with Jetpack Compose Kotlin/Jetpack Compose Composable fun ProductCard(product: Product) { Column( modifier = Modifier.semantics { contentDescription = product.a11y.templates.summary } ) { Text(product.name) Text("$" + product.price) } } Key Features • Uses generated a11y.templates.summary field • Direct semantic mapping from API • Consistent with schema definition Accessibility Benefits • Screen readers get meaningful hints • No manual string building needed • Platform agnostic implementation
  • 22.
    iOS Example SwiftUI Usingaccessibility metadata in SwiftUI components SwiftUI Code SwiftUI automatically uses the a11y.templates.summary for Voice Over announcements. Provides consistent accessibility across all Apple platforms. struct ProductView: View { let product: Product var body: some View { VStack(alignment: .leading) { Text(product.name) .font(.headline) Text("$(product.price)") .font(.subheadline) } .accessibilityLabel(Text(product.a11y.templates.summary)) } }
  • 23.
    Web Example React Usinga11y metadata in React components with ARIA attributes React Component React components can consume a11y metadata directly through aria-label attributes. The a11y.templates.summary provides a consistent, screen readable summary for the entire component. function ProductCard({ product }) { const { name, price, a11y } = product; return ( <div aria-label={a11y.templates.summary} role="group"> <h3>{name}</h3> <p${price}</p> </div> ); }
  • 24.
    Benefits & Impact Whatyou get when accessibility becomes part of your API contract Single Source of Truth Removes Brittle Client Logic Dynamic Per Instance Summaries Optional Field Level Nuance Result: Consistent, accessible experiences across all platforms with less developer effort
  • 25.
    Ownership & Considerations Keyquestions and strategies for successful implementation Who Owns the Directives? 1 Product + Design collaboration Define semantic meaning and user experience 2 Backend engineers Implement directives and resolvers 3 Combination Collaboration of cross-functional teams Missing Metadata Strategy Sensible Fallbacks Use field names as fallback labels when a11y metadata is missing Monitoring Track missing a11y metadata and report gaps Gradual Adoption Start with critical fields, expand coverage over time
  • 26.
    Localization Strategy Template perLocale Use locale-specific templates in schema Server-Side Injection Inject localized strings at runtime Deferred Loading Load translations on demand Versioning & Compatibility Backward Compatibility New directives are optional, existing clients continue to work Schema Evolution Add new metadata fields without breaking changes Feature Flags Enable/disable a11y generation per client Ownership & Considerations Key questions and strategies for successful implementation
  • 27.
    Thank You! Questions? Let'scontinue the conversation github.com/vanessamj99/GraphQL-a11y LinkedIn Portfolio #a11y #APIs Vanessa Johnson - Android Engineer @ The New York Times GitHub