
Native Accessibility Performance
- 1 installs
- 5 repo stars
- Updated August 5, 2026
- bjornmelin/dev-skills
native-accessibility-performance is a skill for Expo/React Native motion accessibility, reduced motion, haptics, and UI-thread performance.
About
This skill handles motion accessibility and UI-thread performance for Expo and React Native apps. Developers use it to add reduced-motion and haptics behavior, audit dropped frames and frame pressure, and validate gestures on device. It includes an audit CLI and a manual device-proof checklist, and defers implementation patterns to adjacent native-motion skills.
- Expo/React Native motion accessibility, reduced motion, and haptics guidance
- Audits UI-thread performance, frame pressure, and gestures
- Includes a self-contained audit CLI and manual device-proof checklist
Native Accessibility Performance by the numbers
- 1 all-time installs (skills.sh)
- Ranked #959 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
native-accessibility-performance capabilities & compatibility
- Capabilities
- frontend · ui design · testing
- Use cases
- frontend · ui design · testing
What native-accessibility-performance says it does
Expo/React Native motion accessibility, reduced motion, haptics, UI-thread performance, frame pressure, gestures, and manual device proof.
Reduced motion should not remove essential progress, focus, pressed, or error feedback.
Per-frame React state updates can destroy native animation performance.
npx skills add https://github.com/bjornmelin/dev-skills --skill native-accessibility-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 5 |
| Last updated | August 5, 2026 |
| Repository | bjornmelin/dev-skills ↗ |
What it does
Adding reduced-motion and haptics accessibility and auditing UI-thread frame performance in an Expo/React Native app.
Who is it for?
Developers adding motion accessibility or fixing UI-thread performance in Expo/React Native apps.
Skip if: Near-miss tasks outside its boundaries owned by adjacent motion or platform skills.
When should I use this skill?
Working on React Native animation performance, reduced motion, haptics, dropped frames, or gestures.
By the numbers
- Ships a scan/doctor audit CLI (scripts/audit.mjs)
- Bundles a manual iOS/Android device-proof checklist
Files
Native Accessibility Performance
Expo/React Native motion accessibility, reduced motion, haptics, UI-thread performance, frame pressure, gestures, and manual device proof.
Operating Contract
Use this skill as a compact router plus domain checklist. Load references only when the current task matches their condition. Do not cite local scrape paths, machine cache paths, or hidden source locations. Verify API details against the target repo's installed package versions before editing.
Source Order
1. Inspect the target repo's installed packages, framework/runtime versions, local design tokens, accessibility policy, and existing motion patterns. 2. Use the bundled references below for skill-specific gotchas and copied source excerpts. 3. Use official current docs/package source as API truth when local code or bundled notes are version-sensitive.
Decision Boundaries
- Prefer native-motion-core for implementation patterns.
- Route command and device validation gates through native-validation.
- Load native-skia or native-three-r3f for canvas/GPU-specific performance.
Workflow
1. Inspect platform, Expo SDK, animation engine, and accessibility setting ownership. 2. Classify UI-thread, JS-thread, layout, and GPU work. 3. Add reduced-motion or static behavior without removing functional feedback. 4. Validate on iOS/Android or document skipped device proof.
Gotchas
- Reduced motion should not remove essential progress, focus, pressed, or error feedback.
- Haptics are feedback, not a substitute for visible state.
- Per-frame React state updates can destroy native animation performance.
<!-- skill-resources:start -->
Bundled Resources
references/reduced-motion-haptics-policy.md- Reduced motion and haptics policy. Read when user preference, haptics, or accessible feedback is involved.references/native-performance-audit.md- Native performance audit guide. Read for JS/UI thread, layout, canvas, GPU, and list animation review.references/manual-device-checks.md- Manual iOS/Android proof checklist. Read before finalizing changes that need simulator/device evidence.references/gesture-feedback-and-motion-sickness.md- Gesture feedback and motion sickness review. Read when gestures, haptics, parallax, carousels, shared transitions, or large native motion can affect comfort.references/frame-budget-instrumentation.md- Native frame budget instrumentation. Read when reports mention dropped frames, slow gestures, list jank, or JS/UI thread contention.references/docs-reanimated-performance.md- Copied source excerpt. Load only when exact upstream wording or API detail is needed.references/docs-rn-accessibility.md- Copied source excerpt. Load only when exact upstream wording or API detail is needed.references/docs-rn-accessibilityinfo.md- Copied source excerpt. Load only when exact upstream wording or API detail is needed.references/docs-rn-performance.md- Copied source excerpt. Load only when exact upstream wording or API detail is needed.references/docs-expo-animation.md- Copied source excerpt. Load only when exact upstream wording or API detail is needed.references/docs-expo-haptics.md- Copied source excerpt. Load only when exact upstream wording or API detail is needed.references/docs-reanimated-accessibility.md- Copied source excerpt. Load only when exact upstream wording or API detail is needed.references/docs-reanimated-source-notes.md- Copied source excerpt. Load only when exact upstream wording or API detail is needed.references/index.md- Complete reference inventory and routing summary.references/source-ledger.md- Source list, checked date, and copy policy.references/provenance.json- Machine-readable source and local-resource metadata.scripts/audit.mjs- Self-contained audit CLI; rundoctorbeforescanwhen setup is unclear.assets/templates/native-accessibility-performance-audit-report.md- Audit response/report template.assets/templates/native-accessibility-performance-review-checklist.md- Manual review checklist.assets/examples/native-accessibility-performance-starter.tsx- Starter fixture/example for this skill.evals/trigger-queries.json- Trigger/near-miss eval set for description tuning.evals/evals.json- Task-quality evals with assertions.
<!-- skill-resources:end -->
Audit CLI
node scripts/audit.mjs doctor --root . --format json
node scripts/audit.mjs scan --root . --format markdown
node scripts/audit.mjs scan --root . --format json --output native-accessibility-performance-audit.jsonTreat script findings as leads. Verify every finding against current code before changing behavior or reporting it as valid.
Closeout
Before finalizing, run the repo's focused validation command, this skill's audit CLI when relevant, and any browser/device/manual proof required by the changed surface. Report commands run, findings fixed, findings skipped with reasons, and residual risk.
interface:
display_name: "Native Accessibility Performance"
short_description: "Native motion accessibility and performance review"
default_prompt: "Use $native-accessibility-performance for Expo/React Native motion accessibility, reduced motion, haptics, UI-thread performance, frame pressure, gestures, and manual device proof."
policy:
allow_implicit_invocation: true
import { AccessibilityInfo } from 'react-native';
/**
* Checks whether the OS-level reduce motion accessibility setting is enabled.
*
* @returns A promise that resolves to true when reduce motion is enabled.
*/
export async function shouldReduceNativeMotion() {
return AccessibilityInfo.isReduceMotionEnabled();
}
Native Accessibility Performance Audit Report
Scope
- Target repo/root:
- Skill: native-accessibility-performance
- Audit command:
- Date:
- Reviewer:
- Installed packages/versions:
- Runtime/platforms checked:
Summary
- High:
- Medium:
- Low:
- Overall status:
Findings
[severity] [ruleId] Short Title
- File:
- Line:
- Confidence:
- Evidence:
- Why it matters for React Native:
- Recommendation:
- Validation required:
- Status:
Validation
- Commands run:
- Source/package versions checked:
- Runtime/browser/device proof:
- Accessibility/reduced-motion proof:
- Skipped checks and reason:
Residual Risk
- Accepted tradeoffs:
- Follow-up owner:
Native Accessibility Performance Review Checklist
Before Editing
- [ ] Confirm this skill owns the task and adjacent skills do not.
- [ ] Inspect installed package/framework/runtime versions.
- [ ] Read the first matching reference from SKILL.md resource routing.
- [ ] Run
node scripts/audit.mjs doctor --root <repo> --format jsonwhen setup is unclear.
Manual Review
- [ ] Inspect platform, Expo SDK, animation engine, and accessibility setting ownership.
- [ ] Classify UI-thread, JS-thread, layout, and GPU work.
- [ ] Add reduced-motion or static behavior without removing functional feedback.
- [ ] Validate on iOS/Android or document skipped device proof.
- [ ] Check gotcha: Reduced motion should not remove essential progress, focus, pressed, or error feedback.
- [ ] Check gotcha: Haptics are feedback, not a substitute for visible state.
- [ ] Check gotcha: Per-frame React state updates can destroy native animation performance.
- [ ] Verify reduced-motion or accessible static fallback when motion is nonessential.
- [ ] Verify cleanup on unmount/navigation or owner teardown.
Closeout
- [ ] Run
node scripts/audit.mjs scan --root <repo> --format markdownwhen auditing. - [ ] Run the target repo's focused validation commands.
- [ ] Capture browser/device/manual proof when the changed surface renders motion.
- [ ] Report fixed findings, skipped findings with reasons, and residual risk.
{
"evals": [
{
"id": "native-accessibility-performance-task-1",
"prompt": "In src/components/Hero.tsx, implement React Native animation performance behavior for a product surface and explain the validation gates.",
"expected_output": "Uses native-accessibility-performance routing, checks installed versions, applies domain-specific React Native guidance, runs or recommends the bundled audit CLI where relevant, and reports validation evidence.",
"assertions": [
"Mentions the relevant installed package/runtime/version checks for React Native.",
"Uses at least one native-accessibility-performance bundled reference or explains why no extra reference was needed.",
"Includes reduced-motion/accessibility or explains why the effect is essential and already covered.",
"Includes cleanup/interruption/runtime validation appropriate to Expo/React Native motion accessibility, reduced motion, haptics, UI-thread performance, frame pressure, gestures, and manual device proof.",
"Does not route the task to an unrelated near-miss skill."
]
},
{
"id": "native-accessibility-performance-task-2",
"prompt": "Review this branch for React Native motion issues: package versions look current but users report jank and missing reduced-motion behavior.",
"expected_output": "Uses native-accessibility-performance routing, checks installed versions, applies domain-specific React Native guidance, runs or recommends the bundled audit CLI where relevant, and reports validation evidence.",
"assertions": [
"Mentions the relevant installed package/runtime/version checks for React Native.",
"Uses at least one native-accessibility-performance bundled reference or explains why no extra reference was needed.",
"Includes reduced-motion/accessibility or explains why the effect is essential and already covered.",
"Includes cleanup/interruption/runtime validation appropriate to Expo/React Native motion accessibility, reduced motion, haptics, UI-thread performance, frame pressure, gestures, and manual device proof.",
"Does not route the task to an unrelated near-miss skill."
]
},
{
"id": "native-accessibility-performance-task-3",
"prompt": "Create a minimal example using reduced motion native that includes cleanup and accessibility notes.",
"expected_output": "Uses native-accessibility-performance routing, checks installed versions, applies domain-specific React Native guidance, runs or recommends the bundled audit CLI where relevant, and reports validation evidence.",
"assertions": [
"Mentions the relevant installed package/runtime/version checks for React Native.",
"Uses at least one native-accessibility-performance bundled reference or explains why no extra reference was needed.",
"Includes reduced-motion/accessibility or explains why the effect is essential and already covered.",
"Includes cleanup/interruption/runtime validation appropriate to Expo/React Native motion accessibility, reduced motion, haptics, UI-thread performance, frame pressure, gestures, and manual device proof.",
"Does not route the task to an unrelated near-miss skill."
]
},
{
"id": "native-accessibility-performance-task-4",
"prompt": "Migrate an existing motion implementation to the right React Native primitive without changing unrelated UI.",
"expected_output": "Uses native-accessibility-performance routing, checks installed versions, applies domain-specific React Native guidance, runs or recommends the bundled audit CLI where relevant, and reports validation evidence.",
"assertions": [
"Mentions the relevant installed package/runtime/version checks for React Native.",
"Uses at least one native-accessibility-performance bundled reference or explains why no extra reference was needed.",
"Includes reduced-motion/accessibility or explains why the effect is essential and already covered.",
"Includes cleanup/interruption/runtime validation appropriate to Expo/React Native motion accessibility, reduced motion, haptics, UI-thread performance, frame pressure, gestures, and manual device proof.",
"Does not route the task to an unrelated near-miss skill."
]
}
]
}
{
"queries": [
{
"query": "Can you help with React Native animation performance in our app and make sure it has cleanup, reduced-motion behavior, and validation?",
"should_trigger": true,
"reason": "React Native animation performance is directly in scope for native-accessibility-performance."
},
{
"query": "Can you help with reduced motion native in our app and make sure it has cleanup, reduced-motion behavior, and validation?",
"should_trigger": true,
"reason": "reduced motion native is directly in scope for native-accessibility-performance."
},
{
"query": "Can you help with AccessibilityInfo in our app and make sure it has cleanup, reduced-motion behavior, and validation?",
"should_trigger": true,
"reason": "AccessibilityInfo is directly in scope for native-accessibility-performance."
},
{
"query": "Can you help with haptics in our app and make sure it has cleanup, reduced-motion behavior, and validation?",
"should_trigger": true,
"reason": "haptics is directly in scope for native-accessibility-performance."
},
{
"query": "Can you help with dropped frames in our app and make sure it has cleanup, reduced-motion behavior, and validation?",
"should_trigger": true,
"reason": "dropped frames is directly in scope for native-accessibility-performance."
},
{
"query": "Can you help with UI thread in our app and make sure it has cleanup, reduced-motion behavior, and validation?",
"should_trigger": true,
"reason": "UI thread is directly in scope for native-accessibility-performance."
},
{
"query": "Review this React Native implementation for expo/react native motion accessibility and produce an audit report.",
"should_trigger": true,
"reason": "The task asks for React Native motion expertise owned by native-accessibility-performance."
},
{
"query": "Audit this Expo animation for AccessibilityInfo reduced-motion handling and haptic feedback boundaries.",
"should_trigger": true,
"reason": "Distinct in-scope trigger wording for native-accessibility-performance."
},
{
"query": "Check this React Native gesture feedback for motion sickness risk and dropped-frame evidence.",
"should_trigger": true,
"reason": "Distinct in-scope trigger wording for native-accessibility-performance."
},
{
"query": "Review this native motion screen for UI-thread performance and accessible static fallback coverage.",
"should_trigger": true,
"reason": "Distinct in-scope trigger wording for native-accessibility-performance."
},
{
"query": "make this API route return paginated JSON from Postgres",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-accessibility-performance."
},
{
"query": "fix this Zod schema for a nested form payload",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-accessibility-performance."
},
{
"query": "write a SQL migration for an orders table",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-accessibility-performance."
},
{
"query": "debug why Clerk middleware redirects on localhost",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-accessibility-performance."
},
{
"query": "convert this CSV file into YAML",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-accessibility-performance."
},
{
"query": "add Open Graph metadata to this marketing page",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-accessibility-performance."
},
{
"query": "optimize a server function cold start without changing UI animation",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-accessibility-performance."
},
{
"query": "write a unit test for a pure date formatter",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-accessibility-performance."
},
{
"query": "create a Dockerfile for this Python service",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-accessibility-performance."
},
{
"query": "review this payment webhook signature verification",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-accessibility-performance."
}
]
}
Expo SDK 56 Animation Notes
Sources: Expo SDK 56 reference, SDK 56 changelog, Expo Reanimated page, Expo UI useNativeState, and Expo Animation docs, checked during the 2026-06-04 audit pass.
Use this file when the target app is Expo-managed or depends on Expo SDK version compatibility.
SDK 56 Baseline
- Expo SDK 56 targets React Native 0.85, React 19.2, Android 7+, Android API
level 36, iOS 16.4+, and Xcode 26.4+.
- The Expo SDK 56 Reanimated page bundles
react-native-reanimated4.3.1 and
installs it with react-native-worklets.
- SDK 56 includes Expo UI stable APIs. Expo UI
useNativeStatecan update
SwiftUI/Jetpack Compose state synchronously from a worklet, but it still depends on the app installing Reanimated and Worklets for those UI-thread paths.
- SDK 56 prebuilt artifacts can speed native builds for common libraries such
as Reanimated, but prebuilt environments also affect whether static feature flags can be changed.
Expo Guidance
- Expo projects can use React Native Animated for basic animation.
- Expo docs recommend
react-native-reanimatedfor advanced, smoother, and
maintainable animation.
- Default Expo templates may already include Reanimated. Existing projects
should install Expo-compatible versions through the repo's Expo command policy, usually expo install through the repo package-manager wrapper.
- In SDK 56, do not hand-pick latest Reanimated/Worklets versions unless the
app is intentionally leaving Expo's compatible set and the native runtime is rebuilt and tested.
Audit Implications
- Do not recommend npm latest for Expo apps. Verify the installed Expo SDK and
Expo-compatible package versions.
- Reanimated setup changes are native-risk. Run the repo's Expo doctor and a
native runtime smoke when Babel/plugin/native build inputs change.
- Reanimated static feature flags are native build inputs and are not mutable
in Expo Go or other prebuilt Reanimated environments.
- Expo UI worklet-native-state changes need the same accessibility review as
Reanimated motion: reduced-motion behavior, focus stability, text input cursor behavior, screen-reader output, and platform proof.
- If the app uses Expo web too, verify reduced-motion behavior on native and web
separately; APIs and haptics differ by platform.
Expo Haptics Notes
Source: Expo SDK 56 Haptics docs, checked during the 2026-06-04 audit pass.
Use this file when animation includes tactile feedback or vibration.
Platform Facts
- Expo SDK 56 bundles
expo-hapticsaround 56.0.x. expo-hapticssupports Android, iOS, and web.- iOS Taptic Engine can do nothing when Low-Power Mode is enabled, the user
disabled Taptic Engine, the camera is active, or dictation is active.
- Web uses the Web Vibration API and depends on browser support, hardware,
permission, and foreground/background behavior.
- On Android, Expo docs recommend
performAndroidHapticsAsyncfor Android
haptic feedback because it uses the device haptics engine and does not require VIBRATE permission.
Accessibility Rules
- Haptics should confirm an intentional user action or outcome. Do not tie them
to decorative loops, autoplay, scroll frames, or every animation tick.
- Avoid high-frequency haptic feedback. Use subtle Android types such as
Segment_Frequent_Tick only when the interaction semantics match and test real hardware.
- Haptic failure must not hide state. Always pair haptics with visual and
accessible state.
- Respect reduced motion and screen-reader context. Reduced motion does not
automatically disable haptics, but tactile output can still become distracting when tied to repeated or decorative movement.
Validation
- Test on real devices when haptics matter; simulators and web may not prove
tactile behavior.
- Record skipped haptic proof explicitly, especially for iOS Low-Power Mode or
Android hardware availability.
- On Android, verify whether
performAndroidHapticsAsyncbetter matches the
semantic action than impactAsync/notificationAsync.
Reanimated Accessibility Notes
Sources: Reanimated 4 accessibility, useReducedMotion, and ReducedMotionConfig docs, checked during the 2026-06-04 audit pass. Expo SDK 56 bundles Reanimated 4.3.1, so verify installed source before relying on later 4.x implementation details.
Use this file when reviewing reduced-motion behavior in Reanimated code.
Reduced Motion Model
Reanimated supports ReduceMotion values:
ReduceMotion.System: follow the device reduced-motion setting.ReduceMotion.Always: disable the animation.ReduceMotion.Never: keep the animation enabled.
By default, Reanimated animations use system reduced-motion behavior. Audit code that overrides this default.
Animation Behavior Under Reduced Motion
withTimingandwithSpringjump to the target value immediately.withDecayreturns the current value immediately, respecting clamp.withDelaystarts the next animation immediately.withRepeatmay not start for infinite/even reversed repetitions, otherwise
it runs once.
withSequenceonly starts children that have reduced motion disabled.- Entering/keyframe/layout animations reach endpoints immediately.
- Exiting animations and shared transitions are omitted.
APIs
- Use per-animation
reduceMotionconfig for specific effects. - Use
.reduceMotion(...)on layout animation builders when layout animation
behavior differs from the default.
useReducedMotion()returns the value from app startup synchronously. It does
not rerender components when the system setting changes.
ReducedMotionConfigapplies globally and should be rare. Prefer local
configuration unless the app has an explicit product-wide policy.
- If the product needs to react when the setting changes during a session, pair
Reanimated with React Native AccessibilityInfo instead of relying only on the startup value from useReducedMotion().
Review Rules
- Treat
ReduceMotion.Neveras a design exception. Verify the animation is
essential and has no static equivalent.
- Do not rely on animation completion callbacks to reveal essential content
under reduced motion unless the reduced-motion behavior is tested.
- For decorative loops, use
Always, a static poster, or no playback. - For large spatial movement, provide a reduced variant rather than only a
faster animation.
Reanimated Performance And Worklets Notes
Sources: Reanimated 4 performance, feature-flag, Worklets threading docs, Expo SDK 56 Reanimated page, Callstack performance workflow, Software Mansion animation slices, and local 4.1.5 package-source checks from the 2026-06-04 audit pass.
Use this file when reviewing Reanimated jank, New Architecture behavior, threading, shared values, or native feature flags.
Runtime Rules
- Keep high-frequency animation math in worklets/UI runtime.
- Avoid reading
sharedValue.valueon the RN/JS runtime in render or hot paths;
derive and consume values in worklets where possible.
- Avoid per-frame
runOnJS, React state writes, logging, or allocation. - In Reanimated 4 with
react-native-worklets, preferscheduleOnRNfor
UI-runtime-to-RN-runtime scheduling when available. If runOnJS remains, import it from react-native-worklets; the Reanimated re-export is deprecated in 4.1.5 source.
- Functions scheduled from UI/runtime code must be defined in RN/JS scope, not
inside the worklet callback.
- Memoize
useFrameCallbackworklets and React Native Gesture Handler gesture
objects unless React Compiler or local code proves stable identity.
- Follow measure, optimize, re-measure, validate. Do not call a memoization or
stale-closure issue unless profiler evidence or a clear stale-read path supports it.
Feature Flags
Reanimated 4 static feature flags require native rebuilds and must match the installed Reanimated/RN versions. Expo SDK 56 bundles Reanimated 4.3.1, but current Reanimated 4.x docs include flags added in 4.2 and 4.4, so installed source is the final authority.
ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPSapplies a fast path for supported
non-layout styles on Android. It can improve smoothness, but transformed touch targets may require gesture-handler Pressable because Fabric touch hit testing may not follow the fast-path visual transform.
IOS_SYNCHRONOUSLY_UPDATE_UI_PROPSis the matching iOS fast path in newer
Reanimated 4.x versions; confirm the installed version before suggesting it.
DISABLE_COMMIT_PAUSING_MECHANISMtargets scroll/jitter issues but is safe
only with React Native's preventShadowTreeCommitExhaustion support enabled in compatible RN versions.
USE_SYNCHRONIZABLE_FOR_MUTABLESis intended to reduce shared-value read
synchronization cost on the RN runtime.
USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS,
FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS, USE_ANIMATION_BACKEND, and IOS_CSS_CORE_ANIMATION are version-sensitive. Confirm availability and release notes/source before recommending them.
- Do not cargo-cult flags. Confirm current docs/source and record the native
rebuild proof. Expo Go and other prebuilt Reanimated environments cannot change static feature flags.
Hot Paths
- Prefer
transformandopacity; layout, shadows, blur, SVG path morphing,
large images, and many animated native views need measurement.
- Memoize gesture objects in list rows unless React Compiler or local patterns
already prove stability.
- Keep list row animation simple. If many elements animate at once, reduce the
effect, virtualize more aggressively, or move drawing-heavy visuals to a canvas strategy.
- Use release or
debugOptimizedbuilds for performance evidence. - Reanimated docs cite rough simultaneous-animation limits: low-end Android can
degrade around 100 animated components, while iOS tolerates more. Treat this as a measurement trigger, not a hard rule.
Red Flags
runOnJSorscheduleOnRNinside scroll/frame callbacks without throttling.useFrameCallbackdoing allocation or JS scheduling every frame.useFrameCallbackorGesture.*objects recreated every render in list rows.withRepeatwithout cancellation or reduced-motion handling.- Transform fast paths applied to pressable/touchable targets without device
touch proof.
- Feature flags changed without iOS/Android rebuild evidence.
- Animated text counters implemented with frequent React state or live-region
updates instead of settled announcements or UI-thread/native text strategies.
Reanimated 4.1.5 Source Notes
Source inspected with opensrc 0.7.2: software-mansion/react-native-reanimated tag 4.1.5. Current Expo SDK 56 docs bundle Reanimated 4.3.1, so use these notes as source-pinned evidence for the inspected version and refresh source for 4.3+ version-specific flags.
Use this file when official docs and installed source need to be reconciled.
Source Findings
ReduceMotionenum values insrc/commonTypes.tsaresystem,always,
and never.
withTiming,withSpring,withDelay,withRepeat,withSequence, and
decay utilities carry reduceMotion through animation config paths.
useReducedMotion()reads the system setting once at module initialization
and returns that startup value.
ReducedMotionConfigsetsReducedMotionManagerglobally while mounted and
restores the previous setting during cleanup.
- Reanimated
src/workletFunctions.tsre-exports worklet APIs from
react-native-worklets; the runOnJS re-export is marked deprecated in favor of direct react-native-worklets import.
react-native-workletsdocs in the same source tree markrunOnJSas
deprecated in favor of scheduleOnRN.
- Reanimated 4.1.5
staticFlags.jsonincludes
DISABLE_COMMIT_PAUSING_MECHANISM, ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS, EXPERIMENTAL_CSS_ANIMATIONS_FOR_SVG_COMPONENTS, and USE_SYNCHRONIZABLE_FOR_MUTABLES, all defaulting to false.
Audit Implications
- Prefer current docs for public API wording, but use installed source to
validate deprecations and default flags.
- Do not assume a feature flag exists unless the installed package contains it.
In particular, Reanimated 4.2+ and 4.4+ docs list flags that are absent from the 4.1.5 source snapshot.
- Do not replace all
runOnJSblindly. First check whether the app has
react-native-worklets with scheduleOnRN; then migrate the specific UI-runtime-to-RN-runtime scheduling call.
- If the target app is Expo SDK 56, check the installed package source before
making assertions about 4.3.1 defaults or later Reanimated docs.
React Native Accessibility Notes
Source: React Native Accessibility docs, checked during the 2026-06-04 audit pass.
Use this file when animation changes semantic content, focus, dynamic text, or screen-reader announcements.
Properties That Matter For Motion
accessiblegroups children into one accessibility element. Animated
grouping changes can alter screen-reader navigation.
accessibilityLabel,accessibilityHint,accessibilityRole, and
accessibilityState must remain true while visual state animates.
accessibilityLiveRegionis Android-only and supportsnone,polite, and
assertive. Use polite for normal dynamic updates; reserve assertive for urgent interruptions.
importantForAccessibilitycan hide decorative or duplicate animated
content from TalkBack.
- Accessibility actions should continue to work when animation changes
collapsed/expanded, reorder, or gesture state.
accessibilityIgnoresInvertColorsis iOS-only and should be used sparingly,
usually for photos or media that should not invert. Do not use it to hide motion contrast problems.
experimental_accessibilityOrdercan change reading order. Animated visual
reorder and explicit accessibility order need screen-reader proof together.
Motion Review Rules
- Do not animate semantic order. If a visual reorder is needed, make sure the
accessibility tree order still matches the user's expected navigation order.
- Avoid animated text churn inside live regions. Prefer a single settled
announcement after the state change.
- Hide decorative animated layers from the accessibility tree when a stable
accessible control or label already communicates the same state.
- Verify dynamic type with animated containers that clip, expand, collapse, or
transform text.
- Keep focus targets stable under transform and feature-flag fast paths; if a
touch target moves visually, test real touch and screen-reader activation.
- Avoid changing
accessible,importantForAccessibility, role, or state as an
animation side effect unless that semantic state truly changed.
Manual Checks
- VoiceOver and TalkBack can enter, operate, and leave the animated surface.
- Reduced motion does not remove essential information.
- Rapid state changes do not queue misleading announcements.
- Dynamic type does not clip or overlap text during and after motion.
React Native AccessibilityInfo Notes
Source: React Native AccessibilityInfo docs, checked during the 2026-06-04 audit pass.
Use this file when code needs to react to accessibility settings at runtime.
Relevant APIs
AccessibilityInfo.isReduceMotionEnabled()returns a promise for the current
reduce-motion setting.
AccessibilityInfo.addEventListener('reduceMotionChanged', handler)fires
when the reduce-motion setting changes. On Android, it also reports true when Developer Options transition animations are off.
AccessibilityInfo.isScreenReaderEnabled()returns whether a screen reader is
active.
AccessibilityInfo.addEventListener('screenReaderChanged', handler)reports
screen-reader setting changes.
AccessibilityInfo.announceForAccessibility(message)posts a screen-reader
announcement.
AccessibilityInfo.announceForAccessibilityWithOptions(message, { queue })
can queue iOS announcements.
AccessibilityInfo.prefersCrossFadeTransitions()reports the iOS preference
for cross-fade transitions when reduce motion is enabled.
AccessibilityInfo.sendAccessibilityEvent(ref, eventType)is the current
imperative focus/event API. React Native 0.85 marks setAccessibilityFocus deprecated in favor of sendAccessibilityEvent(..., 'focus').
Audit Implications
- Use
AccessibilityInfowhen the app must update without restart after the
user toggles reduced motion or a screen reader.
- Always remove subscriptions in cleanup.
- Do not spam
announceForAccessibilityfrom animation frames, loops, scroll
handlers, or rapidly changing counters.
- If an animation changes focus or mounts new content, verify behavior with
VoiceOver/TalkBack rather than relying on static code review.
- If iOS cross-fade preference matters, check
prefersCrossFadeTransitions()
before forcing a custom spatial transition.
Minimal Pattern
useEffect(() => {
let mounted = true;
AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
if (mounted) setReduceMotion(enabled);
}).catch(() => {
if (mounted) setReduceMotion(false);
});
const subscription = AccessibilityInfo.addEventListener(
'reduceMotionChanged',
setReduceMotion,
);
return () => {
mounted = false;
subscription.remove();
};
}, []);React Native Performance Notes
Sources: React Native 0.85 Performance and Animations docs plus Callstack performance workflow slices, checked during the 2026-06-04 audit pass.
Use this file when reviewing frame drops, list animation, JS-thread work, or profiling evidence.
Frame Model
- Smooth UI targets at least 60 FPS, leaving about 16.67 ms per frame.
- React Native exposes separate JS and UI frame rates in the Perf Monitor.
- JS-thread stalls delay React work, touch processing, and JS-driven animation.
- Native/UI-thread scroll and native transitions can continue while JS is busy,
but JS event handling can still lag.
- For high-refresh devices, Reanimated and RN guidance may target 120 FPS, but
120 FPS proof needs device and build support such as iOS ProMotion settings.
Common Motion Problems
- Normal development mode is slower than release. Do not make final performance
calls from regular dev mode alone.
console.*in production paths can become a JS-thread bottleneck.- Large
FlatListsurfaces need list-specific tuning such as stable item
layout, lean row render work, memoized handlers, and measured render windows.
- React Native Animations docs note that long or looping
Animatedanimations
can block VirtualizedList row rendering unless configured with isInteraction: false.
- Heavy work directly in
onPresscan delay feedback; split expensive work
after the immediate visual response.
- Moving transparent text over images on Android can trigger alpha-compositing
cost. Use hardware texture/rasterization only after profiling memory impact.
- Scaling large images is usually cheaper than animating image width/height.
Audit Implications
- Prefer transform/opacity for hot animation paths.
- Profile list scroll, transitions, and gesture surfaces on representative
devices or release-like builds.
- When a motion issue appears only in dev mode, reproduce in release or
debugOptimized before adding complexity.
- Validate that touch responsiveness remains good while the animation runs.
- Measure the exact interaction before and after the fix. Component count,
tree depth, or generic memoization advice is not enough without profiler or behavioral evidence.
Native frame budget instrumentation
Skill: native-accessibility-performance Checked at: 2026-06-04
When To Load
- Read when reports mention dropped frames, slow gestures, list jank, or JS/UI thread contention.
Source Anchors
- https://reactnative.dev/docs/animations
- https://docs.swmansion.com/react-native-reanimated/docs/guides/performance/
Reference Notes
- Classify work by JS thread, UI thread, layout, image decode, list virtualization, and GPU/canvas load before proposing a fix.
- Per-frame React state updates are a common source of animation jank; prefer UI-thread animation values where appropriate.
- Measure on representative devices or simulators when native motion is the user-visible surface.
Focused Checks
- Check development mode versus release/development-build behavior.
- Inspect lists, gestures, images, and expensive derived values near the animation.
Failure Modes
- Treating simulator-only proof as enough for GPU-heavy native surfaces.
- Adding more animation wrappers around a list instead of fixing virtualization/layout work.
Gesture feedback and motion sickness review
Skill: native-accessibility-performance Checked at: 2026-06-04
When To Load
- Read when gestures, haptics, parallax, carousels, shared transitions, or large native motion can affect comfort.
Source Anchors
- https://reactnative.dev/docs/accessibility
- https://docs.swmansion.com/react-native-reanimated/docs/guides/accessibility/
Reference Notes
- Reduced motion should preserve orientation, focus, progress, pressed, success, and error feedback while reducing vestibular movement.
- Haptics can reinforce state but cannot replace visible or screen-reader-accessible feedback.
- Large parallax, zoom, rotation, and full-screen movement deserve stricter device proof than small opacity/scale affordances.
Focused Checks
- Check system reduced-motion setting and any app-level motion preference.
- Verify screen reader labels and focus movement around animated state changes.
Failure Modes
- Removing all feedback for reduced-motion users.
- Using haptics as the only confirmation of a state change.
Operating Guidance
Expo/React Native motion accessibility, reduced motion, haptics, UI-thread performance, frame pressure, gestures, and manual device proof.
Decision Boundaries
- Use native-motion-core for implementation patterns.
- Use native-validation for command and device validation gates.
- Use native-skia or native-three-r3f for canvas/GPU-specific performance.
Workflow Details
1. Inspect platform, Expo SDK, animation engine, and accessibility setting ownership. 2. Classify UI-thread, JS-thread, layout, and GPU work. 3. Add reduced-motion or static behavior without removing functional feedback. 4. Validate on iOS/Android or document skipped device proof.
Gotchas
- Reduced motion should not remove essential progress, focus, pressed, or error feedback.
- Haptics are feedback, not a substitute for visible state.
- Per-frame React state updates can destroy native animation performance.
Validation Notes
- Inspect installed package versions and local architecture before applying examples.
- Prefer the bundled
scripts/audit.mjs doctor --root <repo> --format jsoncommand when setup is unclear. - Use
scripts/audit.mjs scan --root <repo> --format markdownfor repeatable static findings, then manually verify every finding against current code. - Close with repo-specific checks and user-visible runtime proof when this skill affects a rendered surface.
Native Accessibility Performance Reference Index
Read only the file that matches the task. Keep SKILL.md as the router.
Tailored References
reduced-motion-haptics-policy.md- Reduced motion and haptics policy.native-performance-audit.md- Native performance audit guide.manual-device-checks.md- Manual iOS/Android proof checklist.gesture-feedback-and-motion-sickness.md- Gesture feedback and motion sickness review.frame-budget-instrumentation.md- Native frame budget instrumentation.
Copied Source Excerpts
docs-reanimated-performance.mddocs-rn-accessibility.mddocs-rn-accessibilityinfo.mddocs-rn-performance.mddocs-expo-animation.mddocs-expo-haptics.mddocs-reanimated-accessibility.mddocs-reanimated-source-notes.md
Manual iOS/Android proof checklist
Skill: native-accessibility-performance Checked at: 2026-06-04
When To Load
- Read before finalizing changes that need simulator/device evidence.
Operating Guidance
Expo/React Native motion accessibility, reduced motion, haptics, UI-thread performance, frame pressure, gestures, and manual device proof.
Decision Boundaries
- Use native-motion-core for implementation patterns.
- Use native-validation for command and device validation gates.
- Use native-skia or native-three-r3f for canvas/GPU-specific performance.
Workflow Details
1. Inspect platform, Expo SDK, animation engine, and accessibility setting ownership. 2. Classify UI-thread, JS-thread, layout, and GPU work. 3. Add reduced-motion or static behavior without removing functional feedback. 4. Validate on iOS/Android or document skipped device proof.
Gotchas
- Reduced motion should not remove essential progress, focus, pressed, or error feedback.
- Haptics are feedback, not a substitute for visible state.
- Per-frame React state updates can destroy native animation performance.
Validation Notes
- Inspect installed package versions and local architecture before applying examples.
- Prefer the bundled
scripts/audit.mjs doctor --root <repo> --format jsoncommand when setup is unclear. - Use
scripts/audit.mjs scan --root <repo> --format markdownfor repeatable static findings, then manually verify every finding against current code. - Close with repo-specific checks and user-visible runtime proof when this skill affects a rendered surface.
Native performance audit guide
Skill: native-accessibility-performance Checked at: 2026-06-04
When To Load
- Read for JS/UI thread, layout, canvas, GPU, and list animation review.
Operating Guidance
Expo/React Native motion accessibility, reduced motion, haptics, UI-thread performance, frame pressure, gestures, and manual device proof.
Decision Boundaries
- Use native-motion-core for implementation patterns.
- Use native-validation for command and device validation gates.
- Use native-skia or native-three-r3f for canvas/GPU-specific performance.
Workflow Details
1. Inspect platform, Expo SDK, animation engine, and accessibility setting ownership. 2. Classify UI-thread, JS-thread, layout, and GPU work. 3. Add reduced-motion or static behavior without removing functional feedback. 4. Validate on iOS/Android or document skipped device proof.
Gotchas
- Reduced motion should not remove essential progress, focus, pressed, or error feedback.
- Haptics are feedback, not a substitute for visible state.
- Per-frame React state updates can destroy native animation performance.
Validation Notes
- Inspect installed package versions and local architecture before applying examples.
- Prefer the bundled
scripts/audit.mjs doctor --root <repo> --format jsoncommand when setup is unclear. - Use
scripts/audit.mjs scan --root <repo> --format markdownfor repeatable static findings, then manually verify every finding against current code. - Close with repo-specific checks and user-visible runtime proof when this skill affects a rendered surface.
{
"skill": "native-accessibility-performance",
"checked_at": "2026-06-04",
"copy_policy": "Bundled references are tailored copied excerpts or derived notes from official docs/package sources where license permits. Verify installed package versions before applying API examples.",
"upstream_sources": [
{
"label": "React Native accessibility/performance docs and Expo docs."
},
{
"label": "Software Mansion React Native skill notes where license-gated."
},
{
"label": "Agent Skills specification and best practices."
}
],
"tailored_reference_files": [
{
"file": "references/reduced-motion-haptics-policy.md",
"title": "Reduced motion and haptics policy",
"sources": []
},
{
"file": "references/native-performance-audit.md",
"title": "Native performance audit guide",
"sources": []
},
{
"file": "references/manual-device-checks.md",
"title": "Manual iOS/Android proof checklist",
"sources": []
},
{
"file": "references/gesture-feedback-and-motion-sickness.md",
"title": "Gesture feedback and motion sickness review",
"sources": [
"https://reactnative.dev/docs/accessibility",
"https://docs.swmansion.com/react-native-reanimated/docs/guides/accessibility/"
]
},
{
"file": "references/frame-budget-instrumentation.md",
"title": "Native frame budget instrumentation",
"sources": [
"https://reactnative.dev/docs/animations",
"https://docs.swmansion.com/react-native-reanimated/docs/guides/performance/"
]
}
],
"bundled_source_excerpt_files": [
"references/docs-reanimated-performance.md",
"references/docs-rn-accessibility.md",
"references/docs-rn-accessibilityinfo.md",
"references/docs-rn-performance.md",
"references/docs-expo-animation.md",
"references/docs-expo-haptics.md",
"references/docs-reanimated-accessibility.md",
"references/docs-reanimated-source-notes.md"
],
"local_files": [
"SKILL.md",
"scripts/audit.mjs",
"assets/templates",
"assets/examples",
"evals/evals.json",
"evals/trigger-queries.json"
]
}
Reduced motion and haptics policy
Skill: native-accessibility-performance Checked at: 2026-06-04
When To Load
- Read when user preference, haptics, or accessible feedback is involved.
Operating Guidance
Expo/React Native motion accessibility, reduced motion, haptics, UI-thread performance, frame pressure, gestures, and manual device proof.
Decision Boundaries
- Use native-motion-core for implementation patterns.
- Use native-validation for command and device validation gates.
- Use native-skia or native-three-r3f for canvas/GPU-specific performance.
Workflow Details
1. Inspect platform, Expo SDK, animation engine, and accessibility setting ownership. 2. Classify UI-thread, JS-thread, layout, and GPU work. 3. Add reduced-motion or static behavior without removing functional feedback. 4. Validate on iOS/Android or document skipped device proof.
Gotchas
- Reduced motion should not remove essential progress, focus, pressed, or error feedback.
- Haptics are feedback, not a substitute for visible state.
- Per-frame React state updates can destroy native animation performance.
Validation Notes
- Inspect installed package versions and local architecture before applying examples.
- Prefer the bundled
scripts/audit.mjs doctor --root <repo> --format jsoncommand when setup is unclear. - Use
scripts/audit.mjs scan --root <repo> --format markdownfor repeatable static findings, then manually verify every finding against current code. - Close with repo-specific checks and user-visible runtime proof when this skill affects a rendered surface.
native-accessibility-performance Source Ledger
Checked at: 2026-06-04
This ledger is skill-local and portable. It names the upstream sources used for the bundled guidance, copied excerpts, generated evals, examples, and audit rules. It intentionally does not reference local scrape paths or machine cache locations.
Primary Sources
- React Native accessibility/performance docs and Expo docs.
- Software Mansion React Native skill notes where license-gated.
- Agent Skills specification and best practices.
Bundled Source Excerpts
references/docs-reanimated-performance.mdreferences/docs-rn-accessibility.mdreferences/docs-rn-accessibilityinfo.mdreferences/docs-rn-performance.mdreferences/docs-expo-animation.mdreferences/docs-expo-haptics.mdreferences/docs-reanimated-accessibility.mdreferences/docs-reanimated-source-notes.md
Tailored Reference Files
references/reduced-motion-haptics-policy.md- Reduced motion and haptics policyreferences/native-performance-audit.md- Native performance audit guidereferences/manual-device-checks.md- Manual iOS/Android proof checklistreferences/gesture-feedback-and-motion-sickness.md- Gesture feedback and motion sickness reviewreferences/frame-budget-instrumentation.md- Native frame budget instrumentation
Local Additions
references/provenance.jsonrecords source URLs, package facts, and copy policy.scripts/audit.mjsprovides the repeatable static audit CLI for this skill.assets/templates/contains output templates and review checklists.assets/examples/contains small starter examples or fixtures.evals/contains trigger and task-quality evals.
Use official docs and installed package versions as API truth when they conflict with older bundled notes.
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
const profile = {
"skillName": "native-accessibility-performance",
"rules": [
{
"id": "native.runonjs-reanimated-import",
"severity": "medium",
"confidence": "high",
"category": "compatibility",
"pattern": "import\\s+[^;]*\\brunOnJS\\b[^;]*from\\s+['\"]react-native-reanimated['\"]",
"message": "runOnJS is imported through Reanimated; Reanimated 4 source marks that re-export deprecated.",
"recommendation": "Verify installed versions. Prefer scheduleOnRN from react-native-worklets when available, or import runOnJS directly from react-native-worklets."
},
{
"id": "native.motion-missing-reduced-motion",
"severity": "medium",
"confidence": "medium",
"category": "accessibility",
"kind": "fileContainsWithout",
"include": "\\b(withRepeat|withTiming|withSpring|withDecay|withSequence|LayoutAnimation|Animated\\.(timing|spring|decay|loop|sequence|parallel)|useFrameCallback|lottie|Lottie|Moti|Rive|Skia)\\b",
"without": "prefers-reduced-motion|motion-reduce|useReducedMotion|ReducedMotion|ReduceMotion|AccessibilityInfo|reduceMotion",
"message": "Motion code was found without an obvious reduced-motion branch in the same file.",
"recommendation": "Add reduced-motion behavior or document where the effect is already reduced at a higher level."
},
{
"id": "native.repeat-no-cancel",
"severity": "medium",
"confidence": "medium",
"category": "lifecycle",
"kind": "fileContainsWithout",
"include": "\\b(withRepeat|Animated\\.loop)\\(",
"without": "cancelAnimation|useReducedMotion|ReducedMotion|AccessibilityInfo|cleanup|return\\s*\\(\\s*\\)\\s*=>",
"message": "Repeating native animation lacks an obvious cancel or reduced-motion branch.",
"recommendation": "Cancel repeaters on unmount/state change and reduce decorative loops for reduced motion."
},
{
"id": "native.haptics-high-frequency",
"severity": "medium",
"confidence": "low",
"category": "accessibility",
"kind": "fileContainsBothWithout",
"include": "\\bHaptics\\.",
"also": "\\b(withRepeat|useFrameCallback|useAnimatedReaction|onScroll|setInterval|autoplay|loop)\\b",
"without": "debounce|throttle|onPress|onLongPress|selectionAsync|performAndroidHapticsAsync|reduced|reduceMotion|AccessibilityInfo",
"message": "Haptics appear near repeated or high-frequency motion code.",
"recommendation": "Tie haptics to intentional user action, avoid repeated tactile output, and test on real devices."
},
{
"id": "native.assertive-live-region-motion",
"severity": "medium",
"confidence": "low",
"category": "accessibility",
"kind": "fileContainsBoth",
"include": "accessibilityLiveRegion\\s*=\\s*['\"{]?assertive",
"also": "\\b(withRepeat|withTiming|withSpring|useFrameCallback|Animated\\.|setInterval|onScroll)\\b",
"message": "Assertive live region appears near animated or repeated updates.",
"recommendation": "Verify TalkBack output. Prefer polite or settled announcements unless the update is urgent."
},
{
"id": "native.global-reduced-motion-never",
"severity": "medium",
"confidence": "medium",
"category": "accessibility",
"pattern": "ReducedMotionConfig[^\\n]*mode=\\{?ReduceMotion\\.Never",
"message": "Global ReducedMotionConfig disables reduced-motion adaptation.",
"recommendation": "Confirm this is an explicit app-wide accessibility decision; prefer local exceptions for essential motion."
},
{
"id": "native.local-reduced-motion-never",
"severity": "medium",
"confidence": "medium",
"category": "accessibility",
"pattern": "(reduceMotion\\s*:\\s*ReduceMotion\\.Never|\\.reduceMotion\\(\\s*ReduceMotion\\.Never)",
"message": "A local animation opts out of reduced-motion adaptation.",
"recommendation": "Verify this motion is essential and provide a static or lower-motion equivalent where possible."
},
{
"id": "native.accessibilityinfo-listener-no-cleanup",
"severity": "medium",
"confidence": "medium",
"category": "accessibility",
"kind": "fileContainsBothWithout",
"include": "AccessibilityInfo\\.addEventListener\\(",
"also": "(reduceMotionChanged|screenReaderChanged|accessibilityServiceChanged|announcementFinished)",
"without": "\\.remove\\(|removeEventListener|return\\s*\\(\\s*\\)\\s*=>",
"message": "AccessibilityInfo listener lacks an obvious cleanup path.",
"recommendation": "Remove subscriptions on unmount and verify rapid navigation does not leave stale accessibility handlers."
},
{
"id": "native.deprecated-set-accessibility-focus",
"severity": "medium",
"confidence": "high",
"category": "compatibility",
"pattern": "AccessibilityInfo\\.setAccessibilityFocus\\(",
"message": "React Native 0.85 marks setAccessibilityFocus as deprecated.",
"recommendation": "Prefer sendAccessibilityEvent(ref, 'focus') when the installed React Native version supports it."
},
{
"id": "native.announce-from-motion-loop",
"severity": "medium",
"confidence": "low",
"category": "accessibility",
"kind": "fileContainsBoth",
"include": "AccessibilityInfo\\.announceForAccessibility",
"also": "\\b(withRepeat|useFrameCallback|useAnimatedReaction|onScroll|setInterval|requestAnimationFrame|Animated\\.)\\b",
"message": "Screen-reader announcements appear near animated or high-frequency update code.",
"recommendation": "Announce a settled state instead of every frame/tick and verify VoiceOver/TalkBack output."
},
{
"id": "native.shared-value-js-read",
"severity": "medium",
"confidence": "low",
"category": "performance",
"pattern": "[^A-Za-z0-9_]([A-Za-z0-9_]+)\\.value",
"message": "Shared value reads on the JS thread can block or desynchronize if used outside worklets.",
"recommendation": "Confirm this read is inside a worklet; otherwise derive and consume on the UI thread."
},
{
"id": "native.animated-native-driver-false",
"severity": "medium",
"confidence": "low",
"category": "performance",
"pattern": "Animated\\.(timing|spring|decay)\\([\\s\\S]{0,500}useNativeDriver\\s*:\\s*false",
"message": "Animated API is configured without the native driver in a nearby animation config.",
"recommendation": "Verify this is required for unsupported props. Prefer native/UI-thread animation for hot interaction paths."
},
{
"id": "native.animated-native-driver-missing",
"severity": "medium",
"confidence": "low",
"category": "performance",
"kind": "fileContainsWithout",
"include": "Animated\\.(timing|spring|decay)\\(",
"without": "useNativeDriver\\s*:",
"message": "Animated API usage lacks an obvious useNativeDriver decision in the same file.",
"recommendation": "Set useNativeDriver when supported, or document why JS-driven animation is required for unsupported props."
},
{
"id": "native.animated-loop-list-interaction",
"severity": "medium",
"confidence": "low",
"category": "performance",
"kind": "fileContainsBothWithout",
"include": "Animated\\.loop\\(",
"also": "\\b(FlatList|VirtualizedList|SectionList|FlashList)\\b",
"without": "isInteraction\\s*:\\s*false",
"message": "A long or looping Animated animation appears in a list surface without isInteraction false.",
"recommendation": "Verify list row rendering while the animation runs; for non-interaction loops, consider isInteraction: false."
},
{
"id": "native.layout-prop-animation",
"severity": "medium",
"confidence": "medium",
"category": "performance",
"pattern": "\\b(width|height|top|left|right|bottom|margin|marginTop|marginBottom|marginLeft|marginRight|padding|paddingTop|paddingBottom|paddingLeft|paddingRight)\\s*:\\s*(withTiming|withSpring|withDecay)\\(",
"message": "A layout-affecting style is animated directly.",
"recommendation": "Prefer transform/opacity or measure the layout cost on representative devices before accepting this path."
},
{
"id": "native.frame-callback-not-memoized",
"severity": "medium",
"confidence": "low",
"category": "performance",
"kind": "fileContainsWithout",
"include": "useFrameCallback\\(",
"without": "useCallback\\(|React Compiler|reactCompiler|compiler",
"message": "useFrameCallback appears without an obvious stable callback.",
"recommendation": "Memoize frame callbacks unless React Compiler or local patterns prove stable identity."
},
{
"id": "native.list-gesture-not-memoized",
"severity": "medium",
"confidence": "low",
"category": "performance",
"kind": "fileContainsBothWithout",
"include": "\\bGesture\\.[A-Za-z]+\\(",
"also": "\\b(FlatList|FlashList|VirtualizedList|SectionList|renderItem)\\b",
"without": "useMemo\\(|React Compiler|reactCompiler|compiler",
"message": "Gesture objects appear in a list/render path without obvious memoization.",
"recommendation": "Memoize gesture objects in list rows unless compiler support or local profiling shows no reattachment cost."
},
{
"id": "native.inline-worklet-rn-scheduler",
"severity": "medium",
"confidence": "medium",
"category": "compatibility",
"pattern": "(scheduleOnRN|runOnJS)\\(\\s*\\([^)]*\\)\\s*=>",
"message": "A UI-runtime-to-RN/JS scheduler receives an inline function.",
"recommendation": "Define scheduled functions in RN/JS scope, then pass the function reference from the worklet."
},
{
"id": "native.runonjs-invoked-argument",
"severity": "medium",
"confidence": "medium",
"category": "compatibility",
"pattern": "runOnJS\\(\\s*[A-Za-z_$][A-Za-z0-9_$]*\\s*\\(",
"message": "runOnJS appears to receive the result of a function call.",
"recommendation": "Pass the function reference to runOnJS, then provide arguments to the returned callable."
},
{
"id": "native.reanimated-static-flags",
"severity": "medium",
"confidence": "medium",
"category": "validation",
"pattern": "\"staticFeatureFlags\"\\s*:",
"message": "Reanimated static feature flags are configured.",
"recommendation": "Verify every flag exists in the installed Reanimated version, then record native rebuild and iOS/Android runtime proof; Expo Go cannot change these flags."
},
{
"id": "native.package-needs-doctor",
"severity": "medium",
"confidence": "medium",
"category": "validation",
"kind": "packageHasAny",
"packages": [
"react-native-reanimated",
"react-native-worklets",
"react-native-gesture-handler",
"nativewind",
"expo-haptics",
"@shopify/flash-list",
"@shopify/react-native-skia",
"@rive-app/react-native",
"lottie-react-native",
"expo-gl"
],
"message": "Native motion dependencies are present and should be validated with platform-specific checks.",
"recommendation": "Run the repo doctor/typecheck/native smoke commands and record iOS/Android proof."
}
]
};
const skipDirs = new Set([
'.git', 'node_modules', '.next', '.nuxt', 'dist', 'build', 'coverage',
'.expo', '.turbo', '.vercel', '.cache', '.codex', '.agents',
'output', 'tmp', 'temp', 'vendor', 'playwright-report', 'storybook-static',
]);
const skillResourceDirs = new Set(['agents', 'evals', 'references', 'templates']);
const fileExtensions = new Set([
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.css', '.scss', '.sass',
'.html', '.vue', '.svelte', '.json',
]);
const severities = ['low', 'medium', 'high'];
const relevantPackages = [
'expo',
'react',
'react-native',
'react-native-reanimated',
'react-native-worklets',
'react-native-gesture-handler',
'expo-haptics',
'@expo/ui',
'@shopify/flash-list',
'@shopify/react-native-skia',
'@rive-app/react-native',
'lottie-react-native',
'expo-gl',
];
function usage() {
return `Usage:
scripts/audit.mjs scan [--root <path>] [--format markdown|json] [--output <path>] [--max-files <n>]
scripts/audit.mjs doctor [--root <path>] [--format markdown|json]
Options:
--root <path> Target repo root. Defaults to current working directory.
--format <format> markdown or json. Defaults to markdown.
--json Alias for --format json.
--output <path> Optional caller-chosen file path for report output.
--max-files <n> Max files to scan. Defaults to 2000.
--help Show this help.
Examples:
scripts/audit.mjs --json doctor --root .
scripts/audit.mjs scan --root . --format markdown
scripts/audit.mjs scan --root . --format json --output motion-audit.json
Config:
Optional .motion-audit.json at --root supports:
{
"ignoreRules": ["rule-id"],
"ignorePaths": ["generated/", "fixtures/"],
"ignores": [{"ruleId": "rule-id", "path": "src/example.tsx"}]
}
Inline suppression:
// motion-audit-ignore rule-id
// motion-audit-ignore all
`;
}
function requireValue(rest, flag) {
const value = rest.shift();
if (!value || value.startsWith("-")) throw new Error(`${flag} requires a value`);
return value;
}
function parseArgs(argv) {
const args = { command: null, root: process.cwd(), format: 'markdown', output: null, maxFiles: 2000 };
const rest = [...argv];
while (rest.length) {
const arg = rest.shift();
if (arg === '--help' || arg === '-h') args.help = true;
else if (arg === '--json') args.format = 'json';
else if (arg === '--root') args.root = path.resolve(requireValue(rest, arg));
else if (arg === '--format') args.format = requireValue(rest, arg);
else if (arg === '--output') args.output = path.resolve(requireValue(rest, arg));
else if (arg === '--max-files') args.maxFiles = Number(requireValue(rest, arg));
else if (!arg.startsWith('-') && args.command === null) args.command = arg;
else throw new Error(`Unknown argument: ${arg}`);
}
args.command = args.command ?? 'scan';
if (!['scan', 'doctor'].includes(args.command)) throw new Error(`Unknown command: ${args.command}`);
if (!['markdown', 'json'].includes(args.format)) throw new Error(`Unknown format: ${args.format}`);
if (!Number.isFinite(args.maxFiles) || args.maxFiles < 1) throw new Error('--max-files must be a positive number');
return args;
}
function loadConfig(root) {
const file = path.join(root, '.motion-audit.json');
if (!fs.existsSync(file)) return { ignoreRules: [], ignorePaths: [], ignores: [] };
try {
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
return {
ignoreRules: Array.isArray(parsed.ignoreRules) ? parsed.ignoreRules : [],
ignorePaths: Array.isArray(parsed.ignorePaths) ? parsed.ignorePaths : [],
ignores: Array.isArray(parsed.ignores) ? parsed.ignores : [],
};
} catch (error) {
throw new Error(`Failed to parse .motion-audit.json: ${error.message}`);
}
}
function shouldSkipDir(relativePath) {
return relativePath.split(path.sep).some((part) => skipDirs.has(part));
}
function readDirEntries(dir) {
try {
return fs.readdirSync(dir, { withFileTypes: true });
} catch {
return [];
}
}
function listFiles(root, maxFiles) {
const files = [];
const scanningSkillRoot = fs.existsSync(path.join(root, 'SKILL.md'));
function walk(dir) {
if (files.length >= maxFiles) return;
for (const entry of readDirEntries(dir)) {
const full = path.join(dir, entry.name);
const rel = path.relative(root, full);
if (entry.isDirectory()) {
if (scanningSkillRoot && skillResourceDirs.has(rel.split(path.sep)[0])) continue;
if (!shouldSkipDir(rel)) walk(full);
} else if (entry.isFile() && fileExtensions.has(path.extname(entry.name))) {
files.push(full);
}
if (files.length >= maxFiles) return;
}
}
walk(root);
return files;
}
function listPackageFiles(root, maxFiles) {
const files = [];
const scanningSkillRoot = fs.existsSync(path.join(root, 'SKILL.md'));
function walk(dir) {
if (files.length >= maxFiles) return;
for (const entry of readDirEntries(dir)) {
const full = path.join(dir, entry.name);
const rel = path.relative(root, full);
if (entry.isDirectory()) {
if (scanningSkillRoot && skillResourceDirs.has(rel.split(path.sep)[0])) continue;
if (!shouldSkipDir(rel)) walk(full);
} else if (entry.isFile() && entry.name === 'package.json') {
files.push(full);
}
if (files.length >= maxFiles) return;
}
}
walk(root);
return files;
}
function readPackage(root) {
if (!fs.existsSync(root)) return { exists: false, packages: new Set(), versions: {}, scripts: {}, packageFiles: [] };
const packages = new Set();
const versions = {};
const scripts = {};
const packageFiles = [];
for (const file of listPackageFiles(root, 2000)) {
try {
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies, pkg.peerDependencies, pkg.optionalDependencies);
const names = new Set(Object.keys(deps ?? {}));
for (const name of names) packages.add(name);
Object.assign(versions, deps ?? {});
Object.assign(scripts, pkg.scripts ?? {});
packageFiles.push({
file: path.relative(root, file),
packages: names,
versions: deps ?? {},
scripts: pkg.scripts ?? {},
});
} catch {
continue;
}
}
return { exists: packageFiles.length > 0, packages, versions, scripts, packageFiles };
}
function relevantPackageVersions(pkg) {
return Object.fromEntries(
relevantPackages
.filter((name) => Object.prototype.hasOwnProperty.call(pkg.versions, name))
.map((name) => [name, pkg.versions[name]])
);
}
function lineForIndex(text, index) {
return text.slice(0, index).split('\n').length;
}
function excerptForLine(lines, lineNumber) {
return (lines[lineNumber - 1] ?? '').trim().slice(0, 240);
}
function isIgnored(config, ruleId, relativePath, lines, lineNumber) {
if (config.ignoreRules.includes(ruleId)) return true;
if (config.ignorePaths.some((ignored) => relativePath.includes(ignored))) return true;
if (config.ignores.some((entry) => entry?.ruleId === ruleId && typeof entry.path === 'string' && entry.path.length > 0 && relativePath.includes(entry.path))) return true;
const nearby = [lines[lineNumber - 1], lines[lineNumber - 2]].filter(Boolean).join('\n');
return nearby.includes('motion-audit-ignore all') || nearby.includes(`motion-audit-ignore ${ruleId}`);
}
function makeFinding(rule, relativePath, line, excerpt) {
return {
id: `${rule.id}:${relativePath}:${line}`,
ruleId: rule.id,
severity: rule.severity,
confidence: rule.confidence,
category: rule.category,
file: relativePath,
line,
excerpt,
rationale: rule.message,
recommendation: rule.recommendation,
};
}
function uniqueFindings(findings) {
return [...new Map(findings.map((finding) => [finding.id, finding])).values()];
}
function ruleRegex(pattern) {
return new RegExp(pattern, 'gms');
}
const workletHookArgumentCounts = new Map([
['useAnimatedStyle', 1],
['useDerivedValue', 1],
['useAnimatedReaction', 2],
['useAnimatedProps', 1],
['useAnimatedScrollHandler', 1],
['useFrameCallback', 1],
['useAnimatedGestureHandler', 1],
]);
const gestureWorkletCallbackPattern =
/\.(?:onBegin|onStart|onUpdate|onChange|onEnd|onFinalize|onTouchesDown|onTouchesMove|onTouchesUp|onTouchesCancelled)\s*\(/g;
function topLevelArguments(text, openParenIndex) {
const args = [];
let start = openParenIndex + 1;
let depth = 1;
let quote = null;
let escaped = false;
let lineComment = false;
let blockComment = false;
for (let index = openParenIndex + 1; index < text.length; index += 1) {
const char = text[index];
const next = text[index + 1];
if (lineComment) {
if (char === '\n') lineComment = false;
continue;
}
if (blockComment) {
if (char === '*' && next === '/') {
blockComment = false;
index += 1;
}
continue;
}
if (quote) {
if (escaped) escaped = false;
else if (char === '\\') escaped = true;
else if (char === quote) quote = null;
continue;
}
if (char === '/' && next === '/') {
lineComment = true;
index += 1;
continue;
}
if (char === '/' && next === '*') {
blockComment = true;
index += 1;
continue;
}
if (char === '"' || char === "'" || char === '`') {
quote = char;
continue;
}
if (char === '(' || char === '[' || char === '{') {
depth += 1;
continue;
}
if (char === ')' || char === ']' || char === '}') {
depth -= 1;
if (depth === 0 && char === ')') {
args.push({ start, end: index });
break;
}
continue;
}
if (char === ',' && depth === 1) {
args.push({ start, end: index });
start = index + 1;
}
}
return args;
}
function isSharedValueWorkletRead(text, index) {
for (const [hookName, workletArgumentCount] of workletHookArgumentCounts) {
const hookRegex = new RegExp(`\\b${hookName}\\s*\\(`, 'g');
for (const match of text.matchAll(hookRegex)) {
const openParenIndex = match.index + match[0].lastIndexOf('(');
if (openParenIndex > index) break;
const args = topLevelArguments(text, openParenIndex).slice(0, workletArgumentCount);
for (const arg of args) {
if (index < arg.start || index >= arg.end) continue;
const argumentText = text.slice(arg.start, arg.end);
if (/(?:=>|\bfunction\b|['"]worklet['"])/.test(argumentText)) return true;
}
}
}
for (const match of text.matchAll(gestureWorkletCallbackPattern)) {
const openParenIndex = match.index + match[0].lastIndexOf('(');
if (openParenIndex > index) break;
const [callbackArg] = topLevelArguments(text, openParenIndex);
if (!callbackArg || index < callbackArg.start || index >= callbackArg.end) continue;
const argumentText = text.slice(callbackArg.start, callbackArg.end);
if (/(?:=>|\bfunction\b|['"]worklet['"])/.test(argumentText)) return true;
}
return false;
}
function isSharedValueWrite(text, match) {
const fullMatch = match[0] ?? '';
const matchIndex = match.index ?? 0;
const valueOffset = fullMatch.lastIndexOf('.value');
if (valueOffset < 0) return false;
const identifierOffset = fullMatch.search(/[A-Za-z0-9_]+\.value/);
const identifierIndex = matchIndex + (identifierOffset < 0 ? valueOffset : identifierOffset);
const afterValue = text.slice(matchIndex + valueOffset + '.value'.length).trimStart();
if (/^(?:\+\+|--|[+\-*/%]?=(?!=))/.test(afterValue)) return true;
const beforeIdentifier = text.slice(0, identifierIndex).trimEnd();
return beforeIdentifier.endsWith('++') || beforeIdentifier.endsWith('--');
}
function scanRule(rule, file, root, text, config) {
const relativePath = path.relative(root, file);
const lines = text.split('\n');
const findings = [];
if (rule.kind === 'fileContainsWithout' || rule.kind === 'fileContainsBoth' || rule.kind === 'fileContainsBothWithout') {
const includeMatch = ruleRegex(rule.include).exec(text);
const alsoMatch = rule.also ? ruleRegex(rule.also).exec(text) : null;
const withoutMatch = rule.without ? ruleRegex(rule.without).exec(text) : null;
const matches =
rule.kind === 'fileContainsBoth'
? includeMatch && alsoMatch
: rule.kind === 'fileContainsBothWithout'
? includeMatch && alsoMatch && !withoutMatch
: includeMatch && (!rule.also || alsoMatch) && !withoutMatch;
if (matches) {
const index = includeMatch.index;
const line = lineForIndex(text, index);
if (!isIgnored(config, rule.id, relativePath, lines, line)) {
findings.push(makeFinding(rule, relativePath, line, excerptForLine(lines, line)));
}
}
return findings;
}
if (rule.kind === 'packageHasAny') return findings;
const regex = ruleRegex(rule.pattern);
for (const match of text.matchAll(regex)) {
if (
rule.id === 'native.shared-value-js-read' &&
(isSharedValueWorkletRead(text, match.index ?? 0) || isSharedValueWrite(text, match))
) {
continue;
}
const line = lineForIndex(text, match.index ?? 0);
if (!isIgnored(config, rule.id, relativePath, lines, line)) {
findings.push(makeFinding(rule, relativePath, line, excerptForLine(lines, line)));
}
}
return findings;
}
function scan(root, maxFiles) {
if (!fs.existsSync(root)) throw new Error(`Root does not exist: ${root}`);
const config = loadConfig(root);
const files = listFiles(root, maxFiles);
const pkg = readPackage(root);
const findings = [];
for (const rule of profile.rules) {
if (config.ignoreRules.includes(rule.id)) continue;
if (rule.kind === 'packageHasAny') {
for (const packageInfo of pkg.packageFiles) {
const matched = (rule.packages ?? []).filter((name) => packageInfo.packages.has(name));
if (matched.length === 0) continue;
if (isIgnored(config, rule.id, packageInfo.file, [''], 1)) continue;
const matchedWithVersions = matched.map((name) => `${name}@${packageInfo.versions[name] ?? 'unknown'}`);
findings.push({
id: `${rule.id}:${packageInfo.file}:1`,
ruleId: rule.id,
severity: rule.severity,
confidence: rule.confidence,
category: rule.category,
file: packageInfo.file,
line: 1,
excerpt: `matched packages: ${matchedWithVersions.join(', ')}`,
rationale: rule.message,
recommendation: rule.recommendation,
});
}
continue;
}
for (const file of files) {
let text;
try {
text = fs.readFileSync(file, 'utf8');
} catch {
continue;
}
findings.push(...scanRule(rule, file, root, text, config));
}
}
const unique = uniqueFindings(findings);
return {
ok: !unique.some((finding) => finding.severity === 'high'),
profile: profile.skillName,
root,
scannedFiles: files.length,
scannedPackageFiles: pkg.packageFiles.length,
rules: profile.rules.length,
findings: unique,
summary: severities.reduce((acc, severity) => {
acc[severity] = unique.filter((finding) => finding.severity === severity).length;
return acc;
}, {}),
};
}
function doctor(root, maxFiles) {
const pkg = readPackage(root);
return {
ok: fs.existsSync(root),
profile: profile.skillName,
root,
packageJson: pkg.exists,
packageFiles: pkg.packageFiles.length,
relevantPackageVersions: relevantPackageVersions(pkg),
configuredRules: profile.rules.length,
sampleFileCount: fs.existsSync(root) ? listFiles(root, maxFiles).length : 0,
configFile: fs.existsSync(path.join(root, '.motion-audit.json')),
notes: [
'scan is read-only',
'use --format json for machine-readable output',
'findings can be suppressed with .motion-audit.json or inline motion-audit-ignore comments',
],
};
}
function renderMarkdown(result) {
if (result.sampleFileCount !== undefined) {
const versions = Object.entries(result.relevantPackageVersions ?? {})
.map(([name, version]) => ` - ${name}: ${version}`)
.join('\n');
return `# Motion Audit Doctor: ${result.profile}
- Root: ${result.root}
- Package JSON: ${result.packageJson ? 'yes' : 'no'}
- Config file: ${result.configFile ? 'yes' : 'no'}
- Configured rules: ${result.configuredRules}
- Sample file count: ${result.sampleFileCount}
- Relevant package versions:
${versions || ' - (none found)'}
- Status: ${result.ok ? 'ok' : 'failed'}
`;
}
const findings = result.findings
.map((finding) => `## ${finding.severity.toUpperCase()} ${finding.ruleId}
- File: ${finding.file}:${finding.line}
- Confidence: ${finding.confidence}
- Category: ${finding.category}
- Evidence: ${finding.excerpt || '(file-level match)'}
- Rationale: ${finding.rationale}
- Recommendation: ${finding.recommendation}
`)
.join('\n');
return `# Motion Audit Report: ${result.profile}
- Root: ${result.root}
- Scanned files: ${result.scannedFiles}
- Rules: ${result.rules}
- Findings: ${result.findings.length}
- Severity summary: high=${result.summary.high}, medium=${result.summary.medium}, low=${result.summary.low}
- Status: ${result.ok ? 'no high-severity findings' : 'high-severity findings present'}
${findings || 'No findings.'}
`;
}
function emit(result, args) {
const body = args.format === 'json' ? JSON.stringify(result, null, 2) + '\n' : renderMarkdown(result);
if (args.output) {
fs.mkdirSync(path.dirname(args.output), { recursive: true });
fs.writeFileSync(args.output, body);
} else {
process.stdout.write(body);
}
}
try {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
process.stdout.write(usage());
process.exit(0);
}
const result = args.command === 'doctor' ? doctor(args.root, args.maxFiles) : scan(args.root, args.maxFiles);
emit(result, args);
process.exit(result.ok ? 0 : 2);
} catch (error) {
const payload = { ok: false, profile: profile.skillName, error: error.message };
const wantsJson = process.argv.includes('--json') || process.argv.includes('--format') && process.argv.includes('json');
if (wantsJson) process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
else {
console.error(error.message);
console.error(usage());
}
process.exit(1);
}