Core concepts

Accessibility and motion

kara’s components are read by VoiceOver and TalkBack the way the platform’s own controls are, and their motion follows the user’s Reduce Motion setting. For what a component cannot know (what an icon means, where a row leads), one set of props says it on both platforms: SwiftUI’s accessibility modifiers on iOS, Compose semantics on Android.

The props

The stacks, Text, Card, Surface, ListItem, LabeledContent, Link and Pressable take all of them; IconButton, Fab, Button, Avatar, Progress and ActivityIndicator take the ones they can use. They are named as React Native names them.

tsx
<HStack accessibilityHint="open the order" onPress={openOrder} spacing="s12">
  <Icon name="shopping-bag" />
  <Text>Order #1024</Text>
  <Text color="content.secondary">Shipped</Text>
</HStack>
// VoiceOver: "Order #1024, Shipped, Button. Open the order."
// TalkBack:  "Order #1024, Shipped, Button, double tap to open the order."

<IconButton accessibilityLabel="Share" name="share" onPress={share} />
<Text accessibilityRole="header" variant="title2">Orders</Text>
<Icon name="info" /> {/* no label: decorative, hidden from both readers */}
PropTypeDescription
accessibilityHiddenboolean

Hides the view and everything in it from VoiceOver and TalkBack: a decorative image, a label read elsewhere.

accessibilityHintstring

What activating the view does, when its label alone does not say. Phrase it as an action ("open the photo"): iOS reads it after a pause, Android as TalkBack's "Double tap to open the photo", on a pressable view only.

accessibilityLabelstring

What a screen reader says for the view, in place of what it would read from its content. On a view read as one element (accessible, a pressable stack) it replaces what the children would say.

accessibilityRole'button' | 'header' | 'image' | 'link' | 'text'

What the view is. A stack with onPress is a button unless told otherwise; link is announced on iOS only.

accessibilityValuestring

Where the view stands, read after its label: "3 of 5", "On", "50 percent".

accessibleboolean

Reads the view and everything in it as one element: a row with a title and a subtitle, a card.

accessibilityHidden
boolean

Hides the view and everything in it from VoiceOver and TalkBack: a decorative image, a label read elsewhere.

accessibilityHint
string

What activating the view does, when its label alone does not say. Phrase it as an action ("open the photo"): iOS reads it after a pause, Android as TalkBack's "Double tap to open the photo", on a pressable view only.

accessibilityLabel
string

What a screen reader says for the view, in place of what it would read from its content. On a view read as one element (accessible, a pressable stack) it replaces what the children would say.

accessibilityRole
'button' | 'header' | 'image' | 'link' | 'text'

What the view is. A stack with onPress is a button unless told otherwise; link is announced on iOS only.

accessibilityValue
string

Where the view stands, read after its label: "3 of 5", "On", "50 percent".

accessible
boolean

Reads the view and everything in it as one element: a row with a title and a subtitle, a card.

What the components do by themselves

  • A stack with onPress is one button: its children are read as one label, in order, and VoiceOver and TalkBack say “Button”. Pressable is a button too.
  • ListItem, RnListItem and a LabeledContent with a value are read as one element. Link is announced as a link on iOS.
  • IconButton, Fab and FabMenu require an accessibilityLabel: an icon has no words of its own.
  • Icon, Avatar and the glyphs kara draws (a chevron, a close) are decorative and hidden unless they are given a label. Skeleton is always hidden: what is loading is for the screen to say.
  • Snackbar is announced when it appears, and its close button is labelled.
  • The native controls (Button, Switch, Checkbox, Slider, TextField, Picker, Stepper, SegmentedControl) are read by the platform from their own labels and values.

On each platform

PropiOSAndroid
accessibilityLabelaccessibilityLabelcontentDescription, which replaces the children’s on an element read as one
accessibilityHintaccessibilityHint, read after a pausethe click action’s label, “double tap to …”, on a pressable view only
accessibilityRolea trait: isButton, isHeader, isImage, isLink, isStaticTextRole.Button, Role.Image or heading(); no link role
accessibilityValueaccessibilityValuestateDescription
accessibleaccessibilityElement(children: .combine)mergeDescendants
accessibilityHiddenaccessibilityHiddenclearAndSetSemantics {}

Phrase a hint as an action (“open the order”): iOS reads it as a sentence of its own, Android after “double tap to”.

Reduce Motion

With Reduce Motion on (iOS) or Remove animations (Android), nothing in kara slides, scales, turns or loops, and what needs a transition to read fades instead, as Apple and Material both advise. The setting is read live: a user can flip it while the app is open.

ComponentMotionWith motion reduced
entering, exiting, Staggerthe motion presetevery preset fades
animatecolours, opacity and the transformonly the colours and the opacity
Pressableeases down to pressedScaledims to pressedOpacity instead
Snackbarslide or scale in and outfades
Skeletonpulsesholds still
Collapsible (Android)expands, the chevron turnsfades, the chevron snaps
Stepper (iOS)the value rolls to the next onecross-fades

The system controls, sheets and navigation transitions follow the setting themselves. KaraProvider reduceMotion overrides it: always for an app with a setting of its own or a screenshot run, never to ignore it.

Your own animations

The motion props honour the setting by themselves. For an animation of your own, useReducedMotion() says whether motion is reduced, by the device or by the provider. Time it with the duration and easing tokens, picked by what the motion does: fast and smoothOut for something arriving, quick for it leaving. timing(size, curve) is that as a withTiming config, with Reanimated’s own check off (ReduceMotion.Never): Reanimated reads the setting only at launch and never hears of the provider’s, so honour it yourself first.

tsx
import {timing, useReducedMotion} from '@hzblj/kara-ui'
import {useEffect} from 'react'
import {useSharedValue, withTiming} from 'react-native-reanimated'

const Badge = ({count}: {count: number}) => {
  const reduced = useReducedMotion()
  const scale = useSharedValue(1)

  useEffect(() => {
    if (reduced) {
      return // the new number is enough; nothing pops
    }

    scale.value = 0.8
    // Arriving: `fast` on `smoothOut`, with Reanimated's own check off: honoured above already
    scale.value = withTiming(1, timing('fast'))
  }, [count, reduced, scale])
  // ...
}