6. Visual accessibility

Status: draft

Chapter goal

Users change how the screen looks: text gets bigger and bolder, contrast goes up, transparency and motion go down, color stops being a reliable signal. The design was made for one combination of these settings, and the app has to work with all of them. This chapter covers what breaks under each setting and the fix that holds.

The screen

The delivery card from the Leaf demo: a title, a two-line description, a Choose delivery button. At the default text size it fits well.

The delivery card at the default text size has a description and a Choose delivery button.
The delivery card at the default text size. Captured on iPhone 17 Pro Max, iOS 27.0.1.
The same delivery card with a larger system text size wraps across more lines and grows vertically.
The same card at Accessibility Large (.accessibility2), scrolled to show the whole card and its button. Captured on iPhone 17 Pro Max, iOS 27.0.1.

At Accessibility Large, body text is about twice the default size. The description wraps to five lines, and the card grows. That works only because the card has no fixed height.

What breaks

  1. Fixed height. The same text in a card with a fixed 42 pt height is cut off after one line. The user doesn't know there was more.
  2. Color-only error. The email field turns red. Nothing else changes.
  3. Animation-only confirmation. A checkmark flies into the cart. With Reduce Motion on, or if the user looked away, the confirmation is gone.
  4. Text on blur. A translucent toolbar over a product photo. With busy images, the text on it can't be read.
  5. Thin custom font. A 300-weight brand font at 15 pt that ignores Dynamic Type and Bold Text.
Delivery text is truncated inside a fixed-height card and fully visible inside a flexible-height card.
The same string twice. Top: fixed height, clipped. Bottom: flexible height. Captured on iPhone 17 Pro Max, iOS 27.0.1.

Where the settings are

The actual Display & Text Size settings screen, including options for text size and display adaptation.
Settings → Accessibility → Display & Text Size: Bold Text, Larger Text, Reduce Transparency, Increase Contrast, Differentiate Without Color, Smart Invert. Captured on iPhone 17 Pro Max, iOS 27.0.1.
The actual Larger Text settings page with the Larger Accessibility Sizes switch and text size slider.
Larger Text. The five accessibility sizes appear only after Larger Accessibility Sizes is turned on. Captured on iPhone 17 Pro Max, iOS 27.0.1.

Constraints

  • No fixed heights on anything that contains text. Minimum heights are fine. Maximum heights are a bug.
  • No custom font sizes outside Dynamic Type. Custom fonts are allowed if they scale.
  • Color never carries a state alone. Not only when Differentiate Without Color is on. Always.
  • Motion never carries meaning alone. Same rule.

Decision 1: Text grows, layout follows

Verdict: text styles everywhere, no fixed text heights, and a layout switch at accessibility sizes.

Text("Delivery tomorrow between 10 AM and 2 PM")
    .font(.title)
    .fixedSize(horizontal: false, vertical: true)

.fixedSize(horizontal: false, vertical: true) lets the text take all the height it needs inside containers that would otherwise compress it. The real fix is to remove .frame(height:) from the container. This is the demo's "after" version.

Custom fonts scale through relativeTo::

Text("Jasmine tea")
    .font(.custom("Leaf Serif", size: 22, relativeTo: .title2))

Icon sizes and paddings that should grow with text use @ScaledMetric:

@ScaledMetric(relativeTo: .body) private var iconSize = 20.0

UIKit:

titleLabel.font = UIFontMetrics(forTextStyle: .headline)
    .scaledFont(for: UIFont(name: "Leaf Serif", size: 17)!)
titleLabel.adjustsFontForContentSizeCategory = true
titleLabel.numberOfLines = 0

When the row is horizontal, switch to vertical at accessibility sizes (Chapter 2, Decision 1).

Check: Larger Text at the maximum size, on checkout and on every form. No "…" without a way to read the full text. The primary action can be reached by scrolling.

Decision 2: Color plus shape or text

Verdict: every color state also has an icon or text that is visible to everyone.

TextField("Email", text: $email)
    .overlay(RoundedRectangle(cornerRadius: 8).stroke(.red, lineWidth: 2))

Label("Enter a complete email: anna@example.com",
      systemImage: "exclamationmark.circle.fill")
    .foregroundStyle(.red)

Why: about 1 in 12 men and 1 in 200 women have some form of color vision deficiency. Red and green are the most common pair to confuse. A red border tells them nothing. The icon and the text work with any color vision. VoiceOver reads the text too, so the same fix covers it.

accessibilityDifferentiateWithoutColor is useful when a design really has no room for an extra signal, for example colored dots in a chart. Then the dots switch to shapes when the setting is on. For form errors and selection, the extra signal should always be there.

An email field with only a red border, compared with a field that includes an error icon and explanation.
Before: red border only. After: border, icon and instruction. Both fields are editable. Captured on iPhone 17 Pro Max, iOS 27.0.1.

Check: Settings → Accessibility → Display & Text Size → Color Filters → Grayscale. Every state on the screen is still distinguishable.

Decision 3: Contrast and transparency follow the system

Verdict: system colors and materials by default. Custom colors get a High Contrast variant in the asset catalog.

Why: system colors like .primary, .secondary and Color(.separator) adjust to Increase Contrast automatically. Materials like .regularMaterial become solid with Reduce Transparency. Custom hex colors and custom blur don't. Each one is a manual check.

@Environment(\.colorSchemeContrast) private var contrast
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency

toolbar
    .background(reduceTransparency ? AnyShapeStyle(Color(.systemBackground))
                                   : AnyShapeStyle(.regularMaterial))
    .overlay(alignment: .bottom) {
        Divider().opacity(contrast == .increased ? 1 : 0.5)
    }

A WCAG 2.2 AA minimum for text contrast is 4.5:1 for body text and 3:1 for large text and UI components. Check custom colors in both light and dark mode, with Increase Contrast on and off. That's four combinations.

Check: Accessibility Inspector → Color Contrast Calculator on custom colors. Increase Contrast and Reduce Transparency on: text over images stays readable.

Decision 4: Reduce Motion changes the animation, not the result

Verdict: the confirmation is a static state on screen for everyone. Reduce Motion replaces the movement with a fade or with nothing.

@Environment(\.accessibilityReduceMotion) private var reduceMotion

VStack {
    if transferComplete {
        Label("Transfer complete", systemImage: "checkmark.circle.fill")
            .transition(reduceMotion ? .opacity : .scale.combined(with: .opacity))
    }
}
.animation(.default, value: transferComplete)

Why: the common bug is if reduceMotion { Text("Done") } else { Animation() }. Then users without Reduce Motion get only the animation, and if they looked away, they never saw the result. The text should always be there. Only the transition changes.

Reduce Motion targets movement: zooms, slides, parallax, spinning. Fades are acceptable. Vestibular disorders are the main reason people turn it on. Large movement across the screen can cause dizziness and nausea.

The actual Motion settings page with Reduce Motion and related system preferences.
Settings → Accessibility → Motion: Reduce Motion, Vehicle Motion Cues, auto-play controls, Prefer Non-Blinking Cursor, Limit Frame Rate. Captured on iPhone 17 Pro Max, iOS 27.0.1.

Check: with Reduce Motion on, nothing on screen moves across more than a small distance. With it on and off, the "Transfer complete" text is visible after the action.

Decision 5: Images stay images under Smart Invert

Verdict: photos, video and maps are excluded from inversion.

AsyncImage(url: product.photoURL)
    .accessibilityIgnoresInvertColors()

Why: Smart Invert inverts the UI but tries to leave media alone. Custom image views it can't detect get inverted, and a tea photo turns blue-green. UIKit: imageView.accessibilityIgnoresInvertColors = true.

Check: Smart Invert on, scroll the catalog. Product photos look normal.

Testing visual settings together

Settings combine. Larger Text with Bold Text and Increase Contrast is a common combination for low vision. The SwiftUI preview matrix in Chapter 10 covers several of them in one canvas.

AI can review screenshots, but only with context about what should be on screen:

Screenshot: checkout at accessibility3 text size, Bold Text on.
Expected visible: price, delivery address, error text, Pay button.
Report anything clipped, overlapping or missing.

A screenshot review doesn't show VoiceOver behavior. That needs the accessibility tree.

What we don't do

  • We don't test only dark mode and call contrast done. Increase Contrast is a separate setting.
  • We don't add an in-app font size slider. Dynamic Type already does this.
  • We don't remove animations entirely under Reduce Motion when a fade keeps the meaning clearer.

Platform notes

  • SwiftUI: text styles, relativeTo:, @ScaledMetric, ViewThatFits, AnyLayout, environment values for each setting.
  • UIKit: UIFontMetrics, adjustsFontForContentSizeCategory, numberOfLines = 0, traitCollection.preferredContentSizeCategory, UIAccessibility status properties.
  • AppKit: no Dynamic Type in the iOS sense, but window resizing, focus rings and Increase Contrast apply. Chapter 12.
  1. Clipped Large Text: Dynamic Type breaks the layout.
  2. Color Only State: state exists only as color.
  3. Animation Only Feedback: the result exists only as motion.
  4. Dynamic Type Contract: rules for scalable text.
  5. State as Value: state beyond visual style.

Chapter takeaway

The design was made for one set of settings. The app has to work with all of them. No fixed text heights, no color-only states, no motion-only results.

Sources

  1. Apple Human Interface Guidelines: Accessibility
  2. Apple Human Interface Guidelines: Color
  3. Apple Human Interface Guidelines: Motion
  4. Apple Support: Use display and text size preferences
  5. Apple Support: Customize onscreen motion on iPhone
  6. Apple Support: Zoom in on the iPhone screen
  7. Apple Developer: DynamicTypeSize
  8. Apple Developer: ScaledMetric
  9. Apple Developer: UIFontMetrics
  10. W3C: Understanding SC 1.4.3 Contrast (Minimum)
  11. National Eye Institute: Color Blindness