---
title: Accessibility
source: 'https://academia.sh/en/courses/cross-platform-production/accessibility'
course: 'Cross-Platform Production Practice'
language: en
updated: '2026-10-21T05:02:51+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Defines the screen reader, semantic properties, the touch target, and dynamic type'
    - "Explains how accessibility settings change a feature's capabilities"
    - 'Compares how a custom gesture with and without a fallback is classified under a screen reader'
    - 'Measures that a feature can drop to degraded even when the view tree does not change'
---

# Accessibility

Accessibility lets an app do its job under different ways of use such as a screen reader, large text, and touch. The lesson measures that a gesture drops to a menu and works in a degraded way when a screen reader is turned on.

In the previous lesson a configuration value determined the capability. In this lesson the user's own setting determines it. One user has turned on the screen reader, another has enlarged the text size, a third navigates with a switch device instead of touching. These are not rare edge cases; they are part of the app's real conditions. **Accessibility** is the design and implementation practice that lets the app do its job under these conditions too. The lesson first describes the parts of accessibility, then measures how well a gesture works while the screen reader is on.

## The Screen Reader and Semantic Properties

The **screen reader** is an assistive technology offered by the platform that conveys the items on the screen by voice or by touch. The user does not look at the screen; they move between items, and the screen reader says what each item is, its name, and the actions that can be taken. The screen reader does not derive this information from the picture. It reads it from the **semantic properties** the app provides: an item's label, role, state, and actions.

Semantic properties are an information layer separate from appearance. The view draws a card as an image and two texts; the semantic properties describe the same card as "Order card, the title is this, there is an action to delete". If an item has no semantic properties, the screen reader either skips it or reads a meaningless name. For this reason a button without a label, even if it looks complete, does not exist for a screen reader user.

The two snippets below show the semantic properties of the same card row on two platforms. Both give the card's title as the label and offer the delete action as a named action instead of a gesture. The snippets are not measured.

```swift
// Platform code: iOS accessibility action
struct CardRow: View {
    let card: Card
    var body: some View {
        Text(card.title)
            .accessibilityLabel(card.title)
            .accessibilityAction(named: "Delete") { remove(card) }
    }
}
```

```kotlin
// Platform code: Android accessibility action
@Composable
fun CardRow(card: Card) {
    Text(
        text = card.title,
        modifier = Modifier.semantics {
            contentDescription = card.title
            customActions = listOf(CustomAccessibilityAction("Delete") { remove(card); true })
        }
    )
}
```

The same idea is stated with two different APIs on two platforms. This shows that in accessibility too a cross-platform app splits into a shared thought and a platform-specific layer: what is said is common, how it is said belongs to the platform.

## The Touch Target and Dynamic Type

The **touch target** is the area of an item that can be touched with a finger. A small target causes mis-taps for a user with limited motor skills and for any user on the move. Platform guidelines recommend a minimum size for targets; this size varies from platform to platform and the lesson gives no number. What matters is that even if the visual item is drawn small, its touch area can be kept in line with the guideline.

**Dynamic type** is the user enlarging the text size from the system setting. The app follows this setting: the text grows, and the layout the text sits in has to make room for it. A component drawn with a fixed width and fixed height cuts off or overflows the text at a large size. For this reason the layout is designed to be flexible, to adapt to the text. The previous lesson described adapting a component; dynamic type shows that adaptation also comes from the user's own setting.

Both settings can remove a capability a feature needs. While a screen reader is on, a free swipe gesture may not be performed reliably, because the screen reader reserves most gestures for its own navigation. At a large text size a table of fixed width does not fit in a row. The model builds these two drops as two capabilities.

## From Setting to Condition

The model turns accessibility settings into a condition. `conditionsFor(name, settings)` starts with three capabilities: `free-gesture` is the free gesture capability, `fixed-width` is the fixed-width layout capability, and `semantics` is the presence of semantic properties. If the screen reader is on, `free-gesture` drops out of the capabilities; if large text is on, `fixed-width` drops out. The lesson's core is given again below.

```javascript
// operability.mjs — classifies how fully a feature works under a condition
export const LEVELS = ["full", "degraded", "deferred", "disabled"];
const FALLBACKS = ["degraded", "deferred"];

export function feature(name, needs, fallback = {}) {
  return { name, needs, fallback };
}

export function condition(name, has) {
  return { name, has };
}

function judge(item, cond) {
  const missing = item.needs.filter((need) => !cond.has.includes(need)).sort();
  let level = "full";
  for (const need of missing) {
    const given = FALLBACKS.includes(item.fallback[need]) ? item.fallback[need] : "disabled";
    if (LEVELS.indexOf(given) > LEVELS.indexOf(level)) level = given;
  }
  return { level, feature: item.name, condition: cond.name, missing };
}

export function assess(features, conditions) {
  const records = [];
  for (const cond of conditions) {
    for (const item of features) records.push(judge(item, cond));
  }
  return records;
}

export function printLevels(records) {
  for (const r of records) {
    const missing = r.missing.length === 0 ? "-" : r.missing.join(",");
    console.log(`${r.level}: ${r.feature} @ ${r.condition} missing=${missing}`);
  }
}

export function countLevels(records) {
  for (const name of LEVELS) {
    console.log(`${name}: ${records.filter((r) => r.level === name).length}`);
  }
  console.log(`affected: ${records.filter((r) => r.level !== "full").length}`);
}
```

The helper that turns settings into a condition is a lesson-local file. It also contains a function that returns the nodes in the view tree; this function does not look at the setting.

```javascript
// settings.mjs — produces a condition and a node list from accessibility settings
import { condition } from "./operability.mjs";

export function conditionsFor(name, settings = {}) {
  const has = ["free-gesture", "fixed-width", "semantics"];
  const kept = has.filter((item) => {
    if (item === "free-gesture") return !settings.screenReader;
    if (item === "fixed-width") return !settings.largeText;
    return true;
  });
  return condition(name, kept);
}

export function nodesFor(settings = {}) {
  return ["Card", "Label", "Actions"];
}
```

The first measurement is not about operability; it prints the capabilities that the three settings leave.

```javascript
// main.mjs
import { conditionsFor } from "./settings.mjs";

const conditions = [
  conditionsFor("default"),
  conditionsFor("screen-reader", { screenReader: true }),
  conditionsFor("large-text", { largeText: true }),
];
for (const item of conditions) {
  console.log(`${item.name}: ${[...item.has].sort().join(",")}`);
}
```

```text
default: fixed-width,free-gesture,semantics
screen-reader: fixed-width,semantics
large-text: free-gesture,semantics
```

Each setting drops one capability, and `semantics` drops under none of them. Semantic properties are the app's own responsibility; a setting does not remove them.

**SA7.** How the screen reader handles a free gesture varies in detail with the platform and the setting. The model reduces this detail to two states: the gesture either can be performed or cannot. Where the fixed layout overflows at large text is also not in the model; only whether it overflows is built as a capability. This shows the operability rule; it does not replace testing with a real screen reader.

## Two Features Under Three Conditions

The second measurement tests two features under three settings. `CardSwipe` is the gesture that deletes a card by swiping and needs `free-gesture`; its fallback is the action menu, so when the capability is missing it drops to degraded. `PriceTable` is a price table of fixed width and needs `fixed-width`; its fallback is a single-column layout.

```javascript
// main.mjs
import { feature, assess, printLevels } from "./operability.mjs";
import { conditionsFor } from "./settings.mjs";

const features = [
  feature("CardSwipe", ["free-gesture"], { "free-gesture": "degraded" }),
  feature("PriceTable", ["fixed-width"], { "fixed-width": "degraded" }),
];
const conditions = [
  conditionsFor("default"),
  conditionsFor("screen-reader", { screenReader: true }),
  conditionsFor("large-text", { largeText: true }),
];
printLevels(assess(features, conditions));
```

```text
full: CardSwipe @ default missing=-
full: PriceTable @ default missing=-
degraded: CardSwipe @ screen-reader missing=free-gesture
full: PriceTable @ screen-reader missing=-
full: CardSwipe @ large-text missing=-
degraded: PriceTable @ large-text missing=fixed-width
```

Each setting affects only the related feature. The screen reader drops `CardSwipe` to degraded and does not touch `PriceTable`; large text does the opposite. A degraded record is not a feature that does not work: the card can be deleted through the action menu, and the table can be read in a single column.

**SA8.** That these two fallbacks are an action menu and a single-column layout is the lesson's construction. Whether a fallback is actually accessible, that is, whether the menu turns out to be truly usable with a screen reader, is tested separately; the model says only that the fallback is defined.

## The Bounding Measurement

The lesson's focus record is `CardSwipe @ screen-reader`. When the screen reader is turned on, `free-gesture` drops out of the capabilities and the record falls to degraded. In this measurement the nodes of the view tree are printed as well, and `CustomGesture`, a custom gesture with no fallback, is added to the comparison.

```javascript
// main.mjs
import { feature, assess, printLevels, countLevels } from "./operability.mjs";
import { conditionsFor, nodesFor } from "./settings.mjs";

console.log(`node: ${nodesFor({ screenReader: false }).join(",")}`);
console.log(`node: ${nodesFor({ screenReader: true }).join(",")}`);

const features = [
  feature("CardSwipe", ["free-gesture"], { "free-gesture": "degraded" }),
  feature("CustomGesture", ["free-gesture"]),
];
const conditions = [
  conditionsFor("default"),
  conditionsFor("screen-reader", { screenReader: true }),
];
const records = assess(features, conditions);
printLevels(records);
countLevels(records);
```

```text
node: Card,Label,Actions
node: Card,Label,Actions
full: CardSwipe @ default missing=-
full: CustomGesture @ default missing=-
degraded: CardSwipe @ screen-reader missing=free-gesture
disabled: CustomGesture @ screen-reader missing=free-gesture
full: 2
degraded: 1
deferred: 0
disabled: 1
affected: 2
```

The node list is the same under both settings: `Card`, `Label`, and `Actions`. When the screen reader was turned on, not a single node changed in the view tree, yet `CardSwipe` dropped from full to degraded. `CustomGesture`, having no fallback, dropped to disabled; a custom gesture without a fallback does not work at all for a screen reader user.

This result differs from the question of M32/K02. That lesson asked about the effect of an update on a node in the view tree: which node was added, changed, removed. Here no node changed, and the feature dropped all the same. Operability classifies not the shape of the tree but whether the capability exists under the condition. The capability can change while the tree stays fixed, because the capability is the work of the user's setting, not of the tree.

The affected count is two and differs from the four of the opening measurement; the two measurements count different sets.

## Summary

- This lesson's operability: degraded. In the `CardSwipe @ screen-reader` record the `free-gesture` capability drops while the screen reader is on, and the feature keeps working through the action menu.
- When the screen reader was turned on, the nodes in the view tree stayed the same; the feature dropped to degraded independently of the nodes.
- `CustomGesture`, which has no fallback, came out disabled under the same condition; the count showed full 2, degraded 1, deferred 0, disabled 1, and affected 2.
- Large text dropped only the `fixed-width` capability; `PriceTable` dropped to degraded and `CardSwipe` stayed full.
- Semantic properties dropped under no setting; the screen reader reads the label, role, and actions from this layer.

## Next Step

In this lesson the capability dropped because of the user's setting and the feature stayed degraded through a fallback. The first lesson of the next topic, `Offline-First Design`, covers the case where the capability drops from the network: the app keeps working with local data, and a degraded flow takes the place of a full flow.
