Sample report · real schema and renderer, example data for a fictional app

Aperture Journal

Audit ID
a1f0c2e4-8b3d-4a91-9e77-2d6c1b0f4e10
Rule catalog
v2.4.0
Schema
v1.2
Build digest
sha256:8918957ceedbb6d1642aa60a117508ad35baff731e6e6fbaf563a0bd548ef898
readyaudit-bundle-sha256/1 · Golden.app · 13 files · 803 bytes · verified 2026-07-20 09:02:00 UTC
62 / 100
EAA Readiness at 87.5% coverage

IOS v3.4.1 swiftui

  • 1 critical
  • 6 major
  • 9 minor

Computed over 15 of these 16 findings — the other 1 sits on a rule the catalog does not score.

96/96 runs · 4 tasks × 8 variants

87.5% coverage · 104 checks · 61 pass · 16 fail

Listing not live yet — what a credit costs

Interactive PDF export lives in the Mac app.

No category median on this report. That row appears once a category has at least 20 completed audits, and the projection always excludes the audit being reported — otherwise the first customer in a category would be shown their own score as the bar.

Exposure Not legal advice

1 unresolved critical findings map to obligations under:

  • WCAG 1.3.2
  • EN 11.1.3.2
Germany · EAA €100,000 per non-compliant product
France · EAA €50,000 plus repeat / additional obligations
Italy · EAA up to 5% of turnover proportionate to revenue
United States · ADA $25k–$75k+ reported per-suit settlement range

Reported figures, subject to national implementation and change. This is a technical readiness perspective, not legal advice — confirm exposure with counsel.

Coverage

Every active rule, on every screen the crawl reached, with the result. Published separately from the score on purpose — one number is how the app did, the other is how much of the target we actually reached.

87.5%

Checks
104
Pass
61
Fail
16
Not applicable
14
Not testable
13
Rules active
13 / 74
Coverage matrix: 13 rules by 8 screens, each cell a verdict.
Rule RootOnboardingLibraryDetailEditorSettingsPaywallCloud Backup
AP-LBL-001 Interactive element has no accessibility label WCAG 4.1.2 A EN 11.4.1.2 Pass Pass Fail Pass Fail Pass Pass Not testable
AP-LBL-002 Decorative image exposed to assistive technology WCAG 1.1.1 A EN 11.1.1.1 Pass Fail Pass Pass Pass Not applicable Pass Not testable
AP-LBL-004 Visible label text is not contained in the accessibility name WCAG 2.5.3 A EN 11.2.5.3 Pass Pass Pass Pass Pass Pass Fail Not testable
AP-LBL-005 Accessibility label repeats the element's role not scored Pass Pass Fail Pass Pass Pass Pass Not testable
AP-LBL-006 Link or button purpose is not determinable from its name WCAG 2.4.4 A EN 11.2.4.4 Pass Pass Pass Fail Pass Pass Pass Not testable
AP-STA-001 Interactive element exposes no role or trait WCAG 4.1.2 A EN 11.4.1.2 Pass Pass Fail Pass Pass Pass Pass Not testable
AP-STA-004 Disabled control does not expose its disabled state WCAG 4.1.2 A EN 11.4.1.2 Not applicable Not applicable Not applicable Not applicable Pass Pass Fail Not testable
AP-STA-005 Adjustable control exposes no value or adjustment actions WCAG 4.1.2 A EN 11.4.1.2 Not applicable Not applicable Not applicable Not applicable Fail Pass Not applicable Not testable
AP-STR-002 Accessibility reading order does not match the meaningful sequence WCAG 1.3.2 A EN 11.1.3.2 Pass Pass Pass Fail Pass Pass Fail Not testable
AP-STR-007 Screen exposes no accessibility elements at all WCAG 1.3.1 A EN 11.1.3.1 Pass Pass Pass Pass Pass Pass Pass Not testable
AP-TGT-001 Touch target smaller than 44pt WCAG 2.5.8 AA Fail Pass Fail Pass Fail Pass Pass Not testable
AP-TGT-006 Adjacent targets overlap or sit inside each other's spacing allowance WCAG 2.5.8 AA Pass Pass Pass Pass Fail Pass Pass Not testable
AP-FRM-001 Input field has no label or instruction WCAG 3.3.2 A EN 11.3.3.2 Not applicable Fail Not applicable Not applicable Not applicable Pass Pass Not testable
  • Pass — evaluated, threshold met
  • Fail — evaluated, threshold missed; see findings
  • Not applicable — the rule's condition was not present
  • Not testable — we could not reach it; this is the only verdict that lowers coverage

The four tasks, one at a time

Each task is a different walk through the app, not one crawl counted four times. Reached says whether the walk got to the thing the task promises: a first-run-only flow, a second screen, a surface behind sign-in, the last step before a purchase. A task that did not get there says so — and says what it did establish instead. Rows is how many checks in the table above were measured during that walk, so a task cannot claim it was reached without evidence that says the same.

Per-task coverage: runs completed, surfaces recorded, check rows measured, and whether the task was reached.
Task Runs Surfaces Rows Reached
T1 Onboarding 24 / 24 6 13 Yes
The second launch presented a different surface than the first, so this build has a first-run-only flow and it was walked.
T2 Crawl 24 / 24 14 65 Yes
T3 Login 24 / 24 3 13 No
No demo credentials were supplied with this build, so no surface behind sign-in could be reached. The sign-in screen itself was recorded and measured; everything past it is counted as not testable rather than as passing.
T4 Conversion 24 / 24 9 13 Yes
The conversion flow was followed to its last irreversible step: the control named 'Start free trial', which was recorded and measured but deliberately not activated.

These runs are not part of the coverage percentage above. Coverage is arithmetic over the check rows; a second denominator mixed into it would make checking less the cheapest way to score higher.

What we could not reach, and what opens it

These rows are subtracted from coverage rather than quietly dropped: a report that hides the screens it never opened is worth less than one that names them. A named gap with no way past it is still no use — the reader cannot tell a locked door from an engine that opens none — so each one below carries the thing that removes it. The counts are recounted from the matrix above, not asserted here.

  • Behind a sign-in

    13 checks blocked · 1 screen · 12.5 points of coverage

    • Cloud Backup requires a signed-in account and no demo credentials were supplied with the build, so the crawl never reached the screen.

    Send a throwaway demo account with the build

    These screens require an account and none was supplied with the build, so the crawl stopped at the sign-in wall. A disposable demo login reaches them.

    Included once within 30 days of this audit; further re-runs use a credit. The credentials are destroyed with the build when the run ends, like every other artifact.

Any of these needs the build again. We destroy our copy when the run ends, so there is nothing on our side to re-open — submit the build again from the app, which is the only channel that carries one, then send the audit id and the item above from the contact box in the app. The run is treated as a re-measure of this report rather than a new audit: the blocked checks are re-read and the coverage figure reissued. Never e-mail a build — a message can carry the id, not the bundle.

What a pass asserts, rule by rule

A pass is a bounded claim, not a clean bill of health. Each line below is the exact assertion the tick marks in that row make — and nothing wider.

AP-LBL-001 · Interactive element has no accessibility label
Every interactive element in this surface's accessibility tree carries a non-empty name. It does not assert the names are good ones — see AP-LBL-006.
  • Root 6/6 interactive elements named
  • Onboarding 5/5 interactive elements named
  • Detail 7/7 interactive elements named
  • Settings 9/9 interactive elements named
  • Paywall 4/4 interactive elements named
AP-LBL-002 · Decorative image exposed to assistive technology
No image judged decorative is announced by the screen reader on this surface.
  • Root 3/3 exposed images named
  • Library 24/24 exposed images named
  • Detail 2/2 exposed images named
  • Editor 11/11 exposed images named
  • Paywall 1/1 exposed images named
AP-LBL-004 · Visible label text is not contained in the accessibility name
Every control on this surface can be activated by Voice Control using the words a sighted user can read on it.
  • Root 4/4 controls match their visible text
  • Onboarding 3/3 controls match their visible text
  • Library 6/6 controls match their visible text
  • Detail 3/3 controls match their visible text
  • Editor 2/2 controls match their visible text
  • Settings 9/9 controls match their visible text
AP-LBL-005 · Accessibility label repeats the element's role
No label on this surface duplicates the role the trait already announces.
  • Root 6/6 labels free of a repeated role
  • Onboarding 5/5 labels free of a repeated role
  • Detail 7/7 labels free of a repeated role
  • Editor 14/14 labels free of a repeated role
  • Settings 9/9 labels free of a repeated role
  • Paywall 4/4 labels free of a repeated role
AP-LBL-006 · Link or button purpose is not determinable from its name
No control on this surface is named with a word that carries no destination or outcome.
  • Root 6/6 names specific, none generic
  • Onboarding 5/5 names specific, none generic
  • Library 11/11 names specific, none generic
  • Editor 14/14 names specific, none generic
  • Settings 9/9 names specific, none generic
  • Paywall 4/4 names specific, none generic
AP-STA-001 · Interactive element exposes no role or trait
Every tappable element on this surface tells the screen reader what kind of control it is.
  • Root 6/6 tappable elements expose a role
  • Onboarding 5/5 tappable elements expose a role
  • Detail 7/7 tappable elements expose a role
  • Editor 14/14 tappable elements expose a role
  • Settings 9/9 tappable elements expose a role
  • Paywall 4/4 tappable elements expose a role
AP-STA-004 · Disabled control does not expose its disabled state
A screen-reader user is told a control is unavailable before trying to use it.
  • Editor 2/2 disabled controls expose the trait · passed in 3 of 3 attempts
  • Settings 1/1 disabled controls expose the trait · passed in 3 of 3 attempts
AP-STA-005 · Adjustable control exposes no value or adjustment actions
Every adjustable control on this surface can be read and changed with the rotor.
  • Settings 2/2 adjustable controls expose value and increment · passed in 3 of 3 attempts
AP-STR-002 · Accessibility reading order does not match the meaningful sequence
Swiping through this surface reads it in the order it is laid out.
  • Root 6/6 elements in visual reading order · passed in 3 of 3 attempts
  • Onboarding 9/9 elements in visual reading order · passed in 3 of 3 attempts
  • Library 12/12 elements in visual reading order · passed in 3 of 3 attempts
  • Editor 18/18 elements in visual reading order · passed in 3 of 3 attempts
  • Settings 14/14 elements in visual reading order · passed in 3 of 3 attempts
AP-STR-007 · Screen exposes no accessibility elements at all
A screen reader reaches at least one element on this surface instead of finding nothing to announce.
  • Root 6 elements reachable by a screen reader · passed in 3 of 3 attempts
  • Onboarding 9 elements reachable by a screen reader · passed in 3 of 3 attempts
  • Library 12 elements reachable by a screen reader · passed in 3 of 3 attempts
  • Detail 11 elements reachable by a screen reader · passed in 3 of 3 attempts
  • Editor 18 elements reachable by a screen reader · passed in 3 of 3 attempts
  • Settings 14 elements reachable by a screen reader · passed in 3 of 3 attempts
  • Paywall 7 elements reachable by a screen reader · passed in 3 of 3 attempts
AP-TGT-001 · Touch target smaller than 44pt
Every interactive frame measured on this surface is at least 44×44pt. Measured from the accessibility tree, not from the drawn glyph — the tappable area is what matters, and the two are often different.
  • Onboarding smallest target 48pt · required 44pt · 5 measured · passed in 3 of 3 attempts
  • Detail smallest target 44pt · required 44pt · 7 measured · passed in 3 of 3 attempts
  • Settings smallest target 44pt · required 44pt · 9 measured · passed in 3 of 3 attempts
  • Paywall smallest target 50pt · required 44pt · 4 measured · passed in 3 of 3 attempts
AP-TGT-006 · Adjacent targets overlap or sit inside each other's spacing allowance
No two targets on this surface are close enough that the spacing exception in SC 2.5.8 is breached.
  • Root closest pair 72px apart · required 24px · passed in 3 of 3 attempts
  • Onboarding closest pair 96px apart · required 24px · passed in 3 of 3 attempts
  • Library closest pair 40px apart · required 24px · passed in 3 of 3 attempts
  • Detail closest pair 56px apart · required 24px · passed in 3 of 3 attempts
  • Settings closest pair 64px apart · required 24px · passed in 3 of 3 attempts
  • Paywall closest pair 88px apart · required 24px · passed in 3 of 3 attempts
AP-FRM-001 · Input field has no label or instruction
Every input on this surface has a label that survives once the user starts typing.
  • Settings 1/1 inputs labelled, none placeholder-only
  • Paywall 1/1 inputs labelled, none placeholder-only

Rule catalog v2.4.0 · 13 of 74 rules active in this engine release · 61 defined but not yet evaluated, and therefore not counted above

16 findings

Sorted critical first · catalog v2.4.0

AP-STR-002 CRITICAL Critical severity

Accessibility order does not match the layout order

Raised from major This is a modal that covers the whole app. The first element the swipe reaches is the paid subscribe button and the close button is reached last of seven, so the swipe-then-double-tap pattern that opens every VoiceOver session starts a purchase on a screen the user has not heard yet.

  • Paywall
  • subscribeButton
  • voiceover
  • iPhone 13 mini
  • T4 · Conversion

Evidence

frame at 18.4s · step 41 runs/a1f0c2e4/paywall-voiceover-frame.pngruns/a1f0c2e4/paywall-voiceover-swipe.mp4

Illustration, not a screenshot: the app is fictional and this frame is generated from the same pixel rectangles the report publishes.

actual
subscribeButton
elements
'subscribeButton' reached at position 1
expected
paywallHeading
first_divergence_position
1
out_of_order
4

Reproduced in 3 of 3 attempts

Suggested fix

The subscribe button is declared first in the modal's view hierarchy, so it is the first thing VoiceOver lands on and the close button is reached last, at position 7. Give the sheet an explicit reading order, or declare the heading before the action.

VStack {
    heading
    priceRows
    subscribeButton
}
.accessibilityElement(children: .contain)
.accessibilitySortPriority(0)
WCAG 1.3.2 EN 301 549 11.1.3.2
AP-LBL-001 MAJOR Major severity

2 interactive elements have no accessibility label

  • Library
  • sortButton
  • voiceover
  • iPhone 13 mini
  • T2 · Crawl

Evidence

elements
'sortButton' and 'filterButton'
interactive_elements
11
unnamed
2

Reproduced in 1 of 1 attempts

Suggested fix

Give both toolbar icon buttons an accessibilityLabel. The icon alone is announced as 'button' with no name, so a VoiceOver user cannot tell sort from filter.

Button(action: sort) { Image(systemName: "arrow.up.arrow.down") }
    .accessibilityLabel("Sort library")
WCAG 4.1.2 EN 301 549 11.4.1.2
AP-LBL-001 MAJOR Major severity

3 interactive elements have no accessibility label

  • Editor
  • toolCrop
  • xxl-font
  • iPhone 15
  • T2 · Crawl

Evidence

elements
'toolCrop', 'toolRotate' and 'toolHeal'
interactive_elements
14
unnamed
3

Reproduced in 1 of 1 attempts

Suggested fix

The three tool-palette buttons are image-only. Add an accessibilityLabel to each; the tooltip text already in the UI is the right wording.

Button(action: crop) { Image(systemName: "crop") }
    .accessibilityLabel("Crop")
WCAG 4.1.2 EN 301 549 11.4.1.2
AP-LBL-004 MAJOR Major severity

1 control's name does not contain the visible text

  • Paywall
  • subscribeButton
  • voiceover
  • iPhone 13 mini
  • T4 · Conversion

Evidence

controls_with_visible_text
5
elements
'subscribeButton' (visible 'Start free trial', name 'Subscribe')
mismatched
1

Reproduced in 1 of 1 attempts

Suggested fix

Voice Control users say what they see. The button reads 'Start free trial' but is named 'Subscribe', so the spoken command does not activate it. Make the name start with the visible text.

Button("Start free trial", action: subscribe)
    .accessibilityLabel("Start free trial, then 9.99 per month")
WCAG 2.5.3 EN 301 549 11.2.5.3
AP-STA-001 MAJOR Major severity

1 tappable element exposes no control role

  • Library
  • gridCell
  • voiceover
  • iPhone 13 mini
  • T2 · Crawl

Evidence

elements
'gridCell' (role 'other', no traits)
tappable_elements
12
without_role
1

Reproduced in 1 of 1 attempts

Suggested fix

The photo cell responds to a tap but is announced as plain content, so VoiceOver never says it is actionable. Add the button trait.

cell
    .accessibilityAddTraits(.isButton)
    .accessibilityLabel("Photo, 12 March 2026")
WCAG 4.1.2 EN 301 549 11.4.1.2
AP-STR-002 MAJOR Major severity

Accessibility order does not match the layout order

  • Detail
  • shareButton
  • dark
  • iPhone 15
  • T2 · Crawl

Evidence

actual
shareButton
elements
'shareButton' reached at position 3
expected
captionLabel
first_divergence_position
3
out_of_order
4

Reproduced in 3 of 3 attempts

Suggested fix

The action row is declared before the caption in the view hierarchy, so swiping jumps from the photo to the buttons and reaches the caption last. Reorder the container, or set an explicit sort priority.

VStack {
    photo
    caption
    actions
}
.accessibilityElement(children: .contain)
WCAG 1.3.2 EN 301 549 11.1.3.2
AP-FRM-001 MAJOR Major severity

1 input field has no persistent label

  • Onboarding
  • emailField
  • default
  • iPhone 15
  • T1 · Onboarding

Evidence

elements
'emailField' (placeholder 'you@example.com', no label)
inputs
2
placeholder_only
1
without_label
1

Reproduced in 1 of 1 attempts

Suggested fix

The field carries only a placeholder, which disappears as soon as the user types and is never announced as a label. Add an accessibilityLabel and keep the placeholder as the hint.

TextField("you@example.com", text: $email)
    .accessibilityLabel("Email address")
WCAG 3.3.2 EN 301 549 11.3.3.2
AP-LBL-002 MINOR Minor severity

2 images are announced with no name

  • Onboarding
  • illustrationPage1
  • default
  • iPhone 15
  • T1 · Onboarding

Evidence

elements
'illustrationPage1' and 'illustrationPage2'
exposed_without_name
2
images
5

Reproduced in 1 of 1 attempts

Suggested fix

Both onboarding illustrations are decorative and repeat the caption underneath. Hide them from accessibility so VoiceOver does not stop on an unnamed image twice per page.

Image("onboarding-1")
    .accessibilityHidden(true)
WCAG 1.1.1 EN 301 549 11.1.1.1
AP-LBL-005 MINOR Minor severity

2 labels repeat the role the trait already announces

  • Library
  • filterButton
  • voiceover
  • iPhone 13 mini
  • T2 · Crawl

Evidence

elements
'filterButton' and 'sortButton'
labelled_controls
11
repeating_role
2

Reproduced in 1 of 1 attempts

Suggested fix

VoiceOver already appends 'button' from the trait, so these are announced as 'Filter button, button'. Drop the noun from the label.

// before: .accessibilityLabel("Filter button")
.accessibilityLabel("Filter")
AP-LBL-006 MINOR Minor severity

1 control is named with a word that carries no destination

  • Detail
  • metadataLink
  • dark
  • iPhone 15
  • T2 · Crawl

Evidence

elements
'metadataLink' (name 'More')
generic_names
1
named_controls
7

Reproduced in 1 of 1 attempts

Suggested fix

Read out of context in the rotor, 'More' says nothing about where it goes. Name the link after its destination.

Link("More", destination: url)
    .accessibilityLabel("More photo details")
WCAG 2.4.4 EN 301 549 11.2.4.4
AP-STA-004 MINOR Minor severity

1 disabled control does not announce that it is unavailable

  • Paywall
  • restoreButton
  • voiceover
  • iPhone 13 mini
  • T4 · Conversion

Evidence

disabled_controls
1
elements
'restoreButton'
without_trait
1

Reproduced in 3 of 3 attempts

Suggested fix

The restore button is greyed out while the receipt loads but still announces as an ordinary button, so a VoiceOver user taps it and nothing happens. Use .disabled() rather than styling it as dimmed.

Button("Restore purchases", action: restore)
    .disabled(receiptIsLoading)
WCAG 4.1.2 EN 301 549 11.4.1.2
AP-STA-005 MINOR Minor severity

2 adjustable controls expose no value or adjustment action

  • Editor
  • sliderExposure
  • xxl-font
  • iPhone 15
  • T2 · Crawl

Evidence

adjustable_controls
4
elements
'sliderExposure' and 'sliderContrast'
incomplete
2

Reproduced in 3 of 3 attempts

Suggested fix

Both sliders announce their name and nothing else, so a VoiceOver user swiping up hears no current value and no confirmation that the value changed. Publish an accessibilityValue.

Slider(value: $exposure, in: -2...2)
    .accessibilityValue("\(exposure, specifier: "%.1f") stops")
WCAG 4.1.2 EN 301 549 11.4.1.2
AP-TGT-001 MINOR Minor severity

1 touch target is smaller than 44pt

  • Root
  • profileAvatar
  • default
  • iPhone 15
  • T2 · Crawl

Evidence

below_threshold
1
elements
'profileAvatar' (32.0 x 32.0pt)
min_pt
32
required_pt
44
targets
6

Reproduced in 3 of 3 attempts

Suggested fix

The avatar in the navigation bar is a 32pt tap target. Keep the drawn size and widen the hit area with a frame and contentShape.

avatar
    .frame(minWidth: 44, minHeight: 44)
    .contentShape(Rectangle())
WCAG 2.5.8
AP-TGT-001 MINOR Minor severity

3 touch targets are smaller than 44pt

  • Library
  • selectionCheck
  • voiceover
  • iPhone 13 mini
  • T2 · Crawl

Evidence

below_threshold
3
elements
'selectionCheck' (28.0 x 28.0pt), 'sortButton' (32.0 x 32.0pt) and 'filterButton' (32.0 x 32.0pt)
min_pt
28
required_pt
44
targets
12

Reproduced in 3 of 3 attempts

Suggested fix

The per-photo selection check and both toolbar icons are drawn below 44pt. Pad the hit area rather than scaling the glyph.

Button(action: toggle) { Image(systemName: "checkmark.circle") }
    .frame(minWidth: 44, minHeight: 44)
    .contentShape(Rectangle())
WCAG 2.5.8
AP-TGT-001 MINOR Minor severity

5 touch targets are smaller than 44pt

  • Editor
  • toolCrop
  • xxl-font
  • iPhone 15
  • T2 · Crawl

Evidence

below_threshold
5
elements
'toolCrop' (24.0 x 24.0pt), 'toolRotate' (24.0 x 24.0pt), 'toolHeal' (24.0 x 24.0pt), 'toolText' (28.0 x 28.0pt) and 'resetButton' (32.0 x 24.0pt)
min_pt
24
required_pt
44
targets
14

Reproduced in 3 of 3 attempts

Suggested fix

The whole tool palette sits at 24pt. On the smallest supported device this is the densest failing area in the app; give each tool a 44pt hit area and keep the 24pt glyph.

ForEach(tools) { tool in
    Button(action: { select(tool) }) { tool.icon }
        .frame(minWidth: 44, minHeight: 44)
        .contentShape(Rectangle())
}
WCAG 2.5.8
AP-TGT-006 MINOR Minor severity

3 pairs of targets sit closer than 24px apart

  • Editor
  • toolCrop
  • xxl-font
  • iPhone 15
  • T2 · Crawl

Evidence

elements
'toolCrop' / 'toolRotate' (18.0px), 'toolRotate' / 'toolHeal' (18.0px) and 'toolHeal' / 'toolText' (22.0px)
min_center_distance_px
18
pairs_below_threshold
3
required_center_distance_px
24

Reproduced in 3 of 3 attempts

Suggested fix

The tool palette is packed at 18px centre-to-centre, so a tremor or a large fingertip hits the neighbouring tool. Space the row and widen each target together.

HStack(spacing: 16) {
    ForEach(tools) { tool in
        Button(action: { select(tool) }) { tool.icon }
            .frame(width: 44, height: 44)
    }
}