Skip to content

feat: add accessibility adaptation layer#4924

Open
adrcotfas wants to merge 1 commit into@adrcotfas/refactor/tokens_elevationfrom
@adrcotfas/refactor/accesibility
Open

feat: add accessibility adaptation layer#4924
adrcotfas wants to merge 1 commit into@adrcotfas/refactor/tokens_elevationfrom
@adrcotfas/refactor/accesibility

Conversation

@adrcotfas
Copy link
Copy Markdown
Collaborator

@adrcotfas adrcotfas commented May 6, 2026

Motivation

The library's existing reduce-motion support set animation.scale = 0 -- a v2-era flag that components had to explicitly check, with no connection to the MD3 theme.motion spring and duration tokens.

This PR wires OS accessibility into the theme properly. useAccessibleTheme replaces the inline PaperProvider logic, subscribes to reduceMotionChanged, and when active collapses theme.motion to instant springs and zero durations via the new reducedMotion preset. A clean theme.motion.prefersReducedMotion: boolean flag gives components a single readable signal instead of the magic scale === 0 check.

animation.scale is kept and still set to 0 for backward compatibility with components that haven't migrated yet. It is now formally deprecated in favour of theme.motion.prefersReducedMotion.

Two further deprecations are added while here: theme.mode ('adaptive' | 'exact'), which is an MD2 elevation-overlay concept superseded by tonal surface colors in theme.colors.elevation.*; and animation.defaultAnimationDuration, which was never read at runtime.
No component behavior changes. The reduce-motion path is functionally equivalent to before for all existing components. theme.motion.prefersReducedMotion and the reducedMotion preset are foundation plumbing -- they will be consumed when components migrate to theme.motion tokens in the per-component PRs that follow.

Related issue

See https://www.notion.so/callstack/React-Native-Paper-Foundation-for-MD3-Expressive-34c5d027c0f880edba3df107cd35946f?source=copy_link

Merge order:

Test plan

  • yarn typescript -- no new type errors
  • yarn test -- all tests pass

@callstack-bot
Copy link
Copy Markdown

callstack-bot commented May 6, 2026

Hey @adrcotfas, thank you for your pull request 🤗. The documentation from this branch can be viewed here.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

The mobile version of example app from this branch is ready! You can see it here.

Comment thread src/index.tsx
toRawSpring,
} from './theme/tokens/sys/motion';

export { useAccessibleTheme } from './theme/accessibility';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think this belongs as a public API. We should only export what's absolutely necessary. Avoid expanding the API surface unless really needed.

Comment thread src/theme/types/theme.ts
Comment on lines +15 to 24
/** @deprecated Will be removed in a future version. MD3 uses tonal surface colors via `theme.colors.elevation.*`. */
mode?: Mode;
/** @deprecated Use `theme.shapes.*` instead. Will be removed in a future version. */
roundness: number;
/** @deprecated Use `theme.motion.*` instead. Will be removed in a future version. */
animation: {
/** @deprecated Use `theme.motion.prefersReducedMotion` instead. Will be removed in a future version. */
scale: number;
/** @deprecated Use `theme.motion.duration.*` instead. Will be removed in a future version. */
/** @deprecated No-op. Use `theme.motion.duration.*` instead. Will be removed in a future version. */
defaultAnimationDuration?: number;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove deprecated theme properties. We don't want another situation like V2/v3 themes.

If necessary, the migration guide can show a helper that converts old theme to new theme for easier migration.

Comment thread src/index.tsx
export {
expressiveMotion,
standardMotion,
reducedMotion,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it's necessary to expose this.

Comment on lines +93 to 119
export const reducedMotion: MotionConfig = {
spring: {
fast: { spatial: instantSpring, effects: instantSpring },
default: { spatial: instantSpring, effects: instantSpring },
slow: { spatial: instantSpring, effects: instantSpring },
},
easing: motionEasing,
prefersReducedMotion: true,
duration: {
short1: 0,
short2: 0,
short3: 0,
short4: 0,
medium1: 0,
medium2: 0,
medium3: 0,
medium4: 0,
long1: 0,
long2: 0,
long3: 0,
long4: 0,
extraLong1: 0,
extraLong2: 0,
extraLong3: 0,
extraLong4: 0,
},
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure setting all of these to 0/instant animation makes sense for reduce motion. Reduce motion is about reducing motion (e.g., movement, like translation, scale, etc.). Simple animations like opacity and colors should still work. It shouldn't disable animations entirely.

It probably makes more sense to handle it at the component level, because how each component changes its animation in response to this depends on the component, e.g. menu should change to simple fade instead of scale etc.

};
}

export function useAccessibleTheme(theme: Theme, enabled = true): Theme {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be wrapping theme in useTheme. This causes every component using this hook to subscribe to reduce motion.

If this is handled, then it should be done in PaperProvider in a single place so there's a single subscription.

But also, if prefersReducedMotion is in the theme, it conflicts with such an API if it's also handled internally. When the user passes a theme, which will already have prefersReducedMotion, is it also handled internally and overridden?

It probably makes sense to expose a separate prop reduceMotion="auto" or something like that on PaperProvider, then internally it can subscribe and expose the actual value in context. Then, on the component level, components can adapt their animations to reduce motion when necessary.

There is also the problem of the value not being known on initial render, as it's async, so if some component has animation on render, it won't properly respect this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants