| Start Date | 21-08-2024 |
|---|---|
| RFC PR | #2336 |
| Blade Issue | (leave this empty if no issue yet) |
- Summary
- Basic Example
- Motivation
- Detailed Design
- Accessibility
- Drawbacks/Constraints
- Alternatives
- Adoption Strategy
- Open Questions
- References
This RFC covers the API Decisions, Library Comparisons, and Research that the design-system team has done for the motion presets. To know more about why we are doing this, checkout Motivation section below.
import { Fade, Card, CardBody } from '@razorpay/components/blade';
<Fade isVisible={showCard}>
<Card>
<CardBody>{/* Blade Card */}</CardBody>
</Card>
</Fade>;Checkout full API Decisions for Motion Presets
You might have seen our previous RFC on Motion Foundations we wrote in 2022. In that RFC we defined token structure and foundational principles for motion.
As our org grows, we believe our products need to go a bit beyond the functionality. How do we make sure that our consumers really love our products? How do we add that cherry on top, those sprinkles, that pineapple on pizza?
To solve for consumer delight, the blade team is working on simplifying adding motion in our consumer applications and introduce consistent animations to our product.
You can check our detailed design proposal on Motion Refresh (Only accessible to Razorpay Employees)
For building presets, we have to figure out few things like
Important
Library that you might know by the name of framer-motion is now called motion/react. It's an independent project from the company Framer now. Check out the announcement by framer-motion's creator.
I will be using motion/react in this proposal instead of framer-motion. Some older POCs might have references to examples using framer-motion imports.
Note
The API decisions here are only there to give some basic idea on the structure and usage. More accurate props will be updated here later once they are finalised in design.
import { Fade } from '@razorpay/blade/components';
<Fade isVisible={showCard}>
<Card>
<CardBody>
<Text>Fade In/Out Card</Text>
</CardBody>
</Card>
</Fade>;type FadeProps = {
/**
* whether to animate entry and / or exit
*
* @default inout
*/
type: 'in' | 'out' | 'inout';
/**
* What should trigger the motion
*
* @default ['mount']
*/
motionTriggers: ('mount' | 'hover' | 'tap' | 'inView')[];
/**
* Visibility state. Only required when motionTriggers is set to mount
*
* @default true
*/
isVisible?: boolean;
}; |
Screen.Recording.2024-09-06.at.1.07.26.PM.movPreviews are just examples of presets. They don't use actual durations and easings yet |
View API Decision for Slide, Move, and other Entry / Exit Animations
import { Slide } from '@razorpay/blade/components';
<Slide>
<Card>
<CardBody>
<Text>Fade In/Out Card</Text>
</CardBody>
</Card>
</Slide>;type SlideProps = {
/**
* @default inout
*/
type: 'in' | 'out' | 'inout';
/**
* What should trigger the motion
*
* @default ['mount']
*/
motionTriggers: ('mount' | 'inView')[];
/**
* @default 'bottom'
*/
direction: 'top' | 'right' | 'bottom' | 'left';
/**
* Visibility state
*/
isVisible?: boolean;
};import { Move } from '@razorpay/blade/components';
<Move isVisible={showCard}>
<Card>
<CardBody>
<Text>Fade In/Out Card</Text>
</CardBody>
</Card>
</Move>;type MoveProps = {
/**
* @default inout
*/
type: 'in' | 'out' | 'inout';
/**
* What should trigger the motion
*
* @default ['mount']
*/
motionTriggers: ('mount' | 'inView')[];
/**
* Visibility state
*/
isVisible?: boolean;
};import { Scale } from '@razorpay/blade/components';
<Scale isHighlighted>
<Box />
</Scale>;type ScaleProps = {
/**
* @default scale-down
*/
type: 'scale-up' | 'scale-down';
/**
* What should trigger the motion
*
* @default ['hover']
*/
motionTriggers: ('mount' | 'hover' | 'tap' | 'inView')[];
/**
* Controlled state of highlighting.
*
* Only applicable when motionTriggers is no defined
*
* @default undefined - uses motionTriggers to trigger highlight
*/
isHighlighted?: boolean;
};Note
Defining morph as preset is a bit tricky and need to make sure the API is feasible with all possible scenarios. Currently we've done a basic feasibility check but we might change / update the API if we come across some animation that can't be built with this API.
import { AnimatePresence } from 'motion/react';
import {
Button,
Morph
} from '@razorpay/blade/components';
<AnimatePresence>
{
isChatVisible ? (
<Morph layoutId="chat-interface">
<ChatInterface />
</Morph>
) : (
<Morph layoutId="chat-interface">
<Button icon={RazorpayIcon} />
</Morph>
)
}
</AnimatePresence> |
Screen.Recording.2024-09-06.at.2.15.35.PM.movPreviews are just examples of presets. They don't use actual durations and easings yet |
Alternate Morph APIs
import { motion } from 'motion/react';
import { Heading } from '@razorpay/blade/components';
const CardHeading = motion(Heading);
<CardHeading layoutId="card-heading" transition={{ duration: theme.motion.duration.slow }}>Hello, World!</CardHeading>
<CardHeading as="h1" layoutId="card-heading">Hello, World!</CardHeading>Cons:
- We won't be able to preset the animation styles and durations with this approach
- Can lead to inconsistent animations
- Inconsistent with other motion presets so not very intuitive and requires learning motion/react's for syntax
import { Heading, morph } from '@razorpay/blade/components';
const CardHeading = morph(Heading);
<CardHeading layoutId="card-heading">Hello, World!</CardHeading>
<Card>
<CardBody>
<CardHeading as="h1" layoutId="card-heading">Hello, World!</CardHeading>
<Box>
<Text>Other Text</Text>
</Box>
</CardBody>
</Card>Pros:
- Allows presetting animation properties on blade
Cons:
- Inconsistent with other motion presets so not very intuitive
- Comparitively more verbose than suggested API
When we wrap a certain component in AnimateInteractions wrapper from blade, we can animate the children component on interactions of the parent component.
Scale animation can be used indepedently to scale item on certain actions but also inside AnimateInteractions.
In below example, the images scales up when its parent container is hoveredimport {
AnimateInteractions,
Scale
} from '@razorpay/blade/components';
<AnimateInteractions motionTriggers={['hover']}>
<Card>
<CardBody>
<Scale motionTriggers={['on-animate-interactions']}>
<img src="./rajorpay.jpeg" />
</Scale>
</CardBody>
</Card>
</AnimateInteractions>; |
Screen.Recording.2024-09-06.at.1.41.14.PM.movPreviews are just examples of presets. They don't use actual durations and easings yet |
You can also use motionTriggers prop directly on scale to scale up the element on hover / tap, etc.
E.g. in this case, the image scales up on hover of the image
<Scale motionTriggers={['hover']}>
<img src="./rajorpay.jpeg" />
</Scale>
import {
Stagger,
Fade
} from '@razorpay/blade/components';
<Stagger isVisible={showCards}>
<Fade>
<Box />
</Fade>
<Fade>
<Box />
</Fade>
<Fade>
<Box />
</Fade>
</Stagger>;type StaggerProps = {
/**
* Visibility state
*/
isVisible?: boolean;
/**
* What should trigger the motion
*
* @default ['mount']
*/
motionTriggers: ('mount' | 'hover' | 'tap' | 'inView')[];
}; |
Screen.Recording.2024-09-06.at.1.26.42.PM.movPreviews are just examples of presets. They don't use actual durations and easings yet |
- License (Preferrably free to use)
- Hardware Accelarated Animations (Using CSS or WAAPI)
- Easy to implement complex animations
- React Router page transition support
- Morph Animations / Layout Animations
- Small bundle-size
Lets compare some libraries over these ideals-
| Goal | Motion React (formerly known as framer-motion) | Motion One | GSAP | Vanilla CSS Animations |
|---|---|---|---|---|
| License (Preferrably free to use) | ✅ MIT | ✅ MIT | ❌ (Commercial License + Paid Plugins) | ✅ No License |
| Hardware Accelarated Animations | ✅ (Hybrid - WAAPI for some transformations with fallback to JS) Hardware Accelarated POC | ✅ (Built on WAAPI) | ❌ | ✅ |
| Easy to implement complex animations | ✅ (Declarative API) | ❌ | ❌ | ❌ |
| React Router Page Transition | ✅ | ❌ (No native support but can be implemented) | ✅ | ❌ (No native support but can be implemented) |
| Morph Animations / Layout Animations | ✅ Framer Motion POC | ❌ | ✅ | ❌ |
| Small bundle size | ❌ (4.6kb core + 15kb (for base animations) + 10kb (if Morph preset is used)) | ✅ (4kb ) | ❌ (26kb core + features) | ✅ (0kb) |
There is also detailed comparison of these libraries at Motion One Docs - Feature Comparisons
The same presets that we have for Entry / Exit, can be used for page transitions. The exit runs on removal of route, and entry runs on enter of route.
It requires additional wrapper of AnimatePresence around the route. You can check code in POC: Page Transitions with Motion React and React Router.
Goal of the POC was to make sure if its possible to animate some part of the page while keeping the other part of the page stable. It was success with framer motion
Screen.Recording.2024-09-05.at.8.07.21.AM.mov
There is new experimental view transitions API that is available inside a flag in chrome.
Although we explored it, we're not planning to build presets around it yet since
- Lack of browser support in modern browsers
- The syntax being CSS so requires different exploration than our other motion/react's presets
- Rare usecase because its only valid in cross-application navigations such as navigating to dashboard post login
- The syntax of view-transition for MPA has changed in the past and might change again since its not well adopted yet.
Conclusion: Thus we can wait for some time for it to mature and be supported in browsers. Motion React itself might come up with some wrappers on top of their API to support this which will make it easier for us to implement presets
Screen.Recording.2024-08-22.at.11.29.12.AM.mov
Note
While GSAP does offload some work to hardware using CSS, it still requires javascript to work and stops working if JS thread is blocked
Screen.Recording.2024-08-23.at.10.28.36.AM.mov
Screen.Recording.2024-08-23.at.8.25.56.AM.mov
- Stagger Animations POC
- Motion React Layout Animations with Blade Components
- Enhancer Component POC to check API feasibility
On reduced motion setting, we'll stop the motion (internally set duration to 0 for all animations). The UI will continue to work but without motion.
- Motion React as a library will be introduced in customer projects which might increase their bundle size.
We'll be using the reduced bundle size version of motion core m internally for presets to ensure Blade uses minimal bundle size.
Recommended way to load motion/react would be -
// If you're using basic presets like Fade, Move, Slide, Scale, etc
import { domAnimations } from 'motion/react';
export default domAnimations; // 15kb;
// OR
// If you're using previously mentioned presets + `Morph` preset or drag / drop animations from motion/react
import { domMax } from 'motion/react';
export default domMax; // 25kb (includes the 15kb of domAnimations)// Make sure to return the specific export containing the feature bundle.
const loadFeatures = () => import('./features').then((res) => res.default);
// This animation will run when loadFeatures resolves.
function App({ children }) {
return <LazyMotion features={loadFeatures}>{children}</LazyMotion>;
}- Alternative libraries and native CSS solution is compared in Library Comparison Section
- Other alternative is to let consumers do animations
- Since there are less high level primitives available and it has led to inconsistent motion across products, we prefer to simplify building animations while giving out consistency
- We plan to target 1 project this quarter (Q4) to get motion adopted
- The new projects that are built, should be built with motion presets on design and dev
- The earlier project that we have, should use motion presets when they redesign / revamp
- Interactive documentation will be added on blade.razorpay.com explaining how to use each preset
- Close-to-real-life examples will be added in documentation to help give idea on how these presets can be used to build complex real-world animations
-
- We will continue to use React Native Reanimated for now. Similar presets can be built on top of react native reanimated in future
-
Should motion components be imported from
@razorpay/blade/componentslike other components or@razorpay/blade/motion- We'll continue to import from components and utils since they are also components only.
-
- Yes. Although a small one. We're changing the structure of motion easing tokens that we have inorder to make them more scalable and consistent for future usecases.
- We'll be writing a codemod that migrates and maps to the new tokens so almost no to minimal manual changes will be required for this migration. More information will be added in the codemod documentation
-
- We had a long discussion on whether we want to exopse low-level presets or high-level presets. What we realised was that how an element appears depends on a lot of things like which element is it, how big / small is it, what is the context of the product, etc. So we can't define that all elements should fade in or all elements should slide in. It highly depends on the context of the product.
- We realised that consistency on that level, can be brought by building patterns that incorporate motion internally (e.g. Wizard pattern that comes with animation of going from one step to other)
-
- We earlier had an idea of dividing transitions between. 1. page transition, 2. section transition, 3. element transition. The idea was to bring consistency in high level motion like page, section, and thus reduce variations needed in element transition
- Although after exploring this idea, we realised that there is nothing called as "page transition" in most modern application. Here's examples of problems we ran into-
- If you have topnav, sidenav, and only switch between items inside sidenav, your entire page shouldn't transition. Only the workspace part should
- If your page has tabs with every tab routing to different URL, your page (or even your workspace) shouldn't transition. Only the content inside of the tab panels should
- Thus we realised that in the context of transition, everything is an element that have appear and disappear transitions. E.g. SideNav, whenever it comes up whether after reloading the page, going from full page screen to sidenav screen, or after moving from login -> dashboard, should have appear transition. If we move between pages where sidenav stays constant, it shouldn't transition.
-
- Motion React vs Motion One by Matt Perry (Creator of both libraries)
- Motion One vs Other Libraries - Feature Comparison
- Motion One & Browser Performance Guide
- Motion React - Hardware Accelarated Animations
- View Transitions API - MDN
- Web Animations API - MDN
- CSS Triggers - What CSS property triggers which type of render
- Motion React Page Transitions Demo
- Motion React Scroll Animations
- Tailwind
groupanimations

