🍃 react-native-ease
Lightweight declarative animations powered by platform APIs. Uses Core Animation on iOS and Animator on Android — zero JS overhead.
Goals
- Fast — Animations run entirely on native platform APIs (CAAnimation, ObjectAnimator/SpringAnimation). No JS animation loop, no worklets, no shared values.
- Simple — CSS-transition-like API. Set target values, get smooth animations. One component, a few props.
- Lightweight — Minimal native code, no C++ runtime, no custom animation engine. Just a thin declarative wrapper around what the OS already provides.
- Interruptible — Changing values mid-animation smoothly redirects to the new target. No jumps.
Non-Goals
- Complex gesture-driven animations — If you need pan/pinch-driven animations, animation worklets, or shared values across components, use react-native-reanimated.
- Layout animations — Animating width/height/layout changes is not supported.
- Shared element transitions — Use Reanimated or React Navigation's shared element transitions.
- Old architecture — Fabric (new architecture) only.
When to use this vs Reanimated
| Fade in a view | Gesture-driven animations |
| Slide/translate on state change | Complex interpolations |
| Scale/rotate on press | Shared values across components |
| Simple enter animations | Layout animations |
| You want zero config | You need animation worklets |
Installation
npm install react-native-ease
yarn add react-native-ease
Quick Start
import { EaseView } from 'react-native-ease';
function FadeCard({ visible, children }) {
return (
<EaseView
animate={{ opacity: visible ? 1 : 0 }}
transition={{ type: 'timing', duration: 300 }}
style={styles.card}
>
{children}
</EaseView>
);
}
EaseView works like a regular View — it accepts children, styles, and all standard view props. When values in animate change, it smoothly transitions to the new values using native platform animations.
Guide
Timing Animations
Timing animations transition from one value to another over a fixed duration with an easing curve.
<EaseView
animate={{ opacity: isVisible ? 1 : 0 }}
transition={{ type: 'timing', duration: 300, easing: 'easeOut' }}
/>
duration | number | 300 | Duration in milliseconds |
easing | EasingType | 'easeInOut' | Easing curve (preset name or [x1, y1, x2, y2] cubic bezier) |
loop | string | — | 'repeat' restarts from the beginning, 'reverse' alternates direction |
Available easing curves:
'linear' — constant speed
'easeIn' — starts slow, accelerates
'easeOut' — starts fast, decelerates
'easeInOut' — slow start and end, fast middle
[x1, y1, x2, y2] — custom cubic bezier (same as CSS cubic-bezier())
Custom Easing
Pass a [x1, y1, x2, y2] tuple for custom cubic bezier curves. The values correspond to the two control points of the bezier curve, matching the CSS cubic-bezier() function.
<EaseView
animate={{ opacity: isVisible ? 1 : 0 }}
transition={{ type: 'timing', duration: 300, easing: [0.4, 0, 0.2, 1] }}
/>
<EaseView
animate={{ scale: active ? 1.2 : 1 }}
transition={{ type: 'timing', duration: 500, easing: [0.68, -0.55, 0.265, 1.55] }}
/>
x-values (x1, x2) must be between 0 and 1. y-values can exceed this range to create overshoot effects.
Spring Animations
Spring animations use a physics-based model for natural-feeling motion. Great for interactive elements.
<EaseView
animate={{ translateX: isOpen ? 200 : 0 }}
transition={{ type: 'spring', damping: 15, stiffness: 120, mass: 1 }}
/>
damping | number | 15 | Friction — higher values reduce oscillation |
stiffness | number | 120 | Spring constant — higher values mean faster animation |
mass | number | 1 | Mass of the object — higher values mean slower, more momentum |
Spring presets for common feels:
{ type: 'spring', damping: 20, stiffness: 300, mass: 1 }
{ type: 'spring', damping: 12, stiffness: 120, mass: 1 }
{ type: 'spring', damping: 8, stiffness: 200, mass: 1 }
{ type: 'spring', damping: 20, stiffness: 60, mass: 2 }
Disabling Animations
Use { type: 'none' } to apply values immediately without animation. Useful for skipping animations in reduced-motion modes or when you need an instant state change.
<EaseView
animate={{ opacity: isVisible ? 1 : 0 }}
transition={{ type: 'none' }}
/>
onTransitionEnd fires immediately with { finished: true }.
Border Radius
borderRadius can be animated just like other properties. It uses hardware-accelerated platform APIs — ViewOutlineProvider + clipToOutline on Android and layer.cornerRadius + layer.masksToBounds on iOS. Unlike RN's style-based borderRadius (which uses a Canvas drawable on Android), this clips children properly and is GPU-accelerated.
<EaseView
animate={{ borderRadius: expanded ? 0 : 16 }}
transition={{ type: 'timing', duration: 300 }}
style={styles.card}
>
<Image source={heroImage} style={styles.image} />
<Text>Content is clipped to rounded corners</Text>
</EaseView>
When borderRadius is in animate, any borderRadius in style is automatically stripped to avoid conflicts.
Animatable Properties
All properties are set in the animate prop as flat values (no transform array).
<EaseView
animate={{
opacity: 1,
translateX: 0,
translateY: 0,
scale: 1,
scaleX: 1,
scaleY: 1,
rotate: 0,
rotateX: 0,
rotateY: 0,
borderRadius: 0,
}}
/>
scale is a shorthand that sets both scaleX and scaleY. When scaleX or scaleY is also specified, it overrides the scale value for that axis.
You can animate any combination of properties simultaneously. All properties share the same transition config.
Looping Animations
Timing animations can loop infinitely. Use 'repeat' to restart from the beginning or 'reverse' to alternate direction.
<EaseView
initialAnimate={{ opacity: 0.3 }}
animate={{ opacity: 1 }}
transition={{ type: 'timing', duration: 1000, easing: 'easeInOut', loop: 'reverse' }}
/>
<EaseView
initialAnimate={{ translateX: 0 }}
animate={{ translateX: -300 }}
transition={{ type: 'timing', duration: 3000, easing: 'linear', loop: 'repeat' }}
/>
Loop requires initialAnimate to define the starting value. Spring animations do not support looping.
Enter Animations
Use initialAnimate to set starting values. On mount, the view starts at initialAnimate values and animates to animate values.
<EaseView
initialAnimate={{ opacity: 0, translateY: 20 }}
animate={{ opacity: 1, translateY: 0 }}
transition={{ type: 'spring', damping: 15, stiffness: 120, mass: 1 }}
/>
Without initialAnimate, the view renders at the animate values immediately with no animation on mount.
Interruption
Animations are interruptible by default. If you change animate values while an animation is running, it smoothly redirects to the new target from wherever it currently is — no jumping or restarting.
<EaseView
animate={{ translateX: isLeft ? 0 : 200 }}
transition={{ type: 'spring', damping: 15, stiffness: 120, mass: 1 }}
/>
Transform Origin
By default, scale and rotation animate from the view's center. Use transformOrigin to change the pivot point with 0–1 fractions.
<EaseView
animate={{ rotate: isOpen ? 45 : 0 }}
transformOrigin={{ x: 0, y: 0 }}
transition={{ type: 'spring', damping: 12, stiffness: 200, mass: 1 }}
style={styles.card}
/>
<EaseView
animate={{ scale: active ? 1.2 : 1 }}
transformOrigin={{ x: 1, y: 1 }}
transition={{ type: 'spring', damping: 15, stiffness: 120, mass: 1 }}
style={styles.card}
/>
{ x: 0, y: 0 } | Top-left |
{ x: 0.5, y: 0.5 } | Center (default) |
{ x: 1, y: 1 } | Bottom-right |
Style Handling
EaseView accepts all standard ViewStyle properties. If a property appears in both style and animate, the animated value takes priority and the style value is stripped. A dev warning is logged when this happens.
<EaseView
animate={{ translateY: moved ? -10 : 0 }}
transition={{ type: 'spring', damping: 15, stiffness: 120, mass: 1 }}
style={{
opacity: 0.9,
backgroundColor: 'white',
borderRadius: 16,
padding: 16,
}}
>
<Text>Notification card</Text>
</EaseView>
<EaseView
animate={{ opacity: 1 }}
style={{ opacity: 0.5, backgroundColor: 'white' }}
/>
API Reference
<EaseView>
A View that animates property changes using native platform APIs.
animate | AnimateProps | Target values for animated properties |
initialAnimate | AnimateProps | Starting values for enter animations (animates to animate on mount) |
transition | Transition | Animation configuration (timing, spring, or none) |
onTransitionEnd | (event) => void | Called when all animations complete with { finished: boolean } |
transformOrigin | { x?: number; y?: number } | Pivot point for scale/rotation as 0–1 fractions. Default: { x: 0.5, y: 0.5 } (center) |
useHardwareLayer | boolean | Android only — rasterize to GPU texture during animations. See Hardware Layers. Default: false |
style | ViewStyle | Non-animated styles (layout, colors, borders, etc.) |
children | ReactNode | Child elements |
| ...rest | ViewProps | All other standard View props |
AnimateProps
opacity | number | 1 | View opacity (0–1) |
translateX | number | 0 | Horizontal translation in pixels |
translateY | number | 0 | Vertical translation in pixels |
scale | number | 1 | Uniform scale factor (shorthand for scaleX + scaleY) |
scaleX | number | 1 | Horizontal scale factor (overrides scale for X axis) |
scaleY | number | 1 | Vertical scale factor (overrides scale for Y axis) |
rotate | number | 0 | Z-axis rotation in degrees |
rotateX | number | 0 | X-axis rotation in degrees (3D) |
rotateY | number | 0 | Y-axis rotation in degrees (3D) |
borderRadius | number | 0 | Border radius in pixels (hardware-accelerated, clips children) |
Properties not specified in animate default to their identity values.
TimingTransition
{
type: 'timing';
duration?: number;
easing?: EasingType;
loop?: 'repeat' | 'reverse';
}
SpringTransition
{
type: 'spring';
damping?: number;
stiffness?: number;
mass?: number;
}
NoneTransition
{
type: 'none';
}
Applies values instantly with no animation. onTransitionEnd fires immediately with { finished: true }.
Hardware Layers (Android)
Setting useHardwareLayer rasterizes the view into a GPU texture for the duration of the animation. This means animated property changes (opacity, scale, rotation) are composited on the RenderThread without redrawing the view hierarchy — useful for complex views with many children.
<EaseView
animate={{ opacity: isVisible ? 1 : 0 }}
useHardwareLayer
/>
Trade-offs:
- Faster rendering for opacity, scale, and rotation animations (RenderThread compositing).
- Uses additional GPU memory for the off-screen texture (proportional to view size).
- Children that overflow the view's layout bounds are clipped by the texture. This causes visual artifacts when animating
translateX/translateY on views with overflowing content.
No-op on iOS where Core Animation already composites off the main thread.
How It Works
EaseView is a native Fabric component. The JS side flattens your animate and transition props into flat native props. When those props change, the native view:
- Diffs previous vs new values to find what changed
- Reads the current in-flight value (for smooth interruption)
- Creates a platform-native animation from the current value to the new target
- Sets the final value immediately on the model layer
On iOS, this uses CABasicAnimation/CASpringAnimation on CALayer key paths. On Android, this uses ObjectAnimator/SpringAnimation on View properties. No JS thread involvement during the animation.
Requirements
- React Native 0.76+ (new architecture / Fabric)
- iOS 15.1+
- Android minSdk 24+
Contributing
License
MIT
Made with create-react-native-library