Skip to content

Commit 4fb0d59

Browse files
JonasBaandrewshie-sentry
authored andcommitted
motion: add first class framer support (#102531)
Add first class framer motion support to theme. @gggritso I'll let you experiment with the transition definition for the drawer animation and we'll figure out if we can modify one of our current definitions or introduce a brand new one, but I'd like to defer that until we see what the desired transition would look like.
1 parent a196875 commit 4fb0d59

File tree

3 files changed

+97
-63
lines changed

3 files changed

+97
-63
lines changed

static/app/components/core/principles/motion/motion.mdx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,35 @@ On an otherwise static page, any motion will draw user attention. For this reaso
5050

5151
# Token Reference
5252

53-
Motion tokens for CSS are exposed under the `theme.motion` namespace as pairs of easing and duration values.
53+
Motion tokens are exposed under the `theme.motion` namespace and support both CSS and Framer Motion usage patterns.
5454

5555
```js
5656
const theme = useTheme();
57-
5857
// motion[easing][duration]
5958
theme.motion.smooth.moderate;
6059
```
6160

61+
## Framer Motion
62+
63+
For Framer Motion animations, use the `framer` namespace which provides transition objects ready to use with Framer Motion components:
64+
65+
```tsx
66+
import {useTheme} from '@emotion/react';
67+
import {motion} from 'framer-motion';
68+
69+
function AnimatedComponent() {
70+
const theme = useTheme();
71+
72+
return (
73+
<motion.div
74+
initial={{opacity: 0, y: -16}}
75+
animate={{opacity: 1, y: 0}}
76+
transition={theme.motion.framer.enter.fast}
77+
/>
78+
);
79+
}
80+
```
81+
6282
## Easing
6383

6484
The easing curve of an animation drastically changes our perception of it. These easing tokens have been chosen to provide snappy, natural motion to interactions.

static/app/stories/playground/motion.tsx

Lines changed: 14 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {Text} from 'sentry/components/core/text';
1111
import * as Storybook from 'sentry/stories';
1212

1313
type Motion = Theme['motion'];
14-
type Duration = keyof Motion['enter'];
15-
type Easing = keyof Motion;
14+
type Duration = keyof Motion['framer']['smooth'];
15+
type Easing = keyof Motion['framer'];
1616

1717
const animations = ['x', 'y', 'scale', 'rotate'] as const;
1818

@@ -44,14 +44,20 @@ export function MotionPlayground() {
4444
<Grid columns="160px 192px" gap="lg" align="center" justify="center">
4545
<Control label="Easing">
4646
<CompactSelect
47-
options={extractTokens(tokens).map(value => ({value, label: value}))}
47+
options={(['smooth', 'snap', 'enter', 'exit'] as const).map(value => ({
48+
value,
49+
label: value,
50+
}))}
4851
value={easing}
4952
onChange={opt => setEasing(opt.value)}
5053
/>
5154
</Control>
5255
<Control label="Duration">
5356
<CompactSelect
54-
options={extractTokens(tokens.enter).map(value => ({value, label: value}))}
57+
options={(['fast', 'moderate', 'slow'] as const).map(value => ({
58+
value,
59+
label: value,
60+
}))}
5561
value={duration}
5662
onChange={opt => setDuration(opt.value)}
5763
/>
@@ -103,15 +109,16 @@ interface CreateAnimationOptions {
103109
property: (typeof animations)[number];
104110
tokens: Motion;
105111
}
112+
106113
function createAnimation({
107114
property,
108-
duration: durationKey,
115+
duration,
109116
easing,
110117
tokens,
111118
}: CreateAnimationOptions): HTMLMotionProps<'div'> {
112119
const delay = 1;
113120
const defaultState = {x: 0, y: 0, opacity: 1, scale: 1, rotate: 0};
114-
const {duration, ease} = extractDurationAndEase(tokens[easing][durationKey]);
121+
const transition = tokens.framer[easing][duration];
115122

116123
return {
117124
initial: {
@@ -123,8 +130,7 @@ function createAnimation({
123130
...makeTargetState({property, state: 'end', easing}),
124131
},
125132
transition: {
126-
ease,
127-
duration,
133+
...transition,
128134
delay,
129135
repeat: Infinity,
130136
repeatDelay: delay,
@@ -176,52 +182,10 @@ const TARGET_CONFIGS: Record<string, TargetConfig> = {
176182
y: TARGET_AXIS,
177183
};
178184

179-
function extractDurationAndEase(css: string): {
180-
duration: number;
181-
ease: [number, number, number, number];
182-
} {
183-
const re =
184-
/^\s*(\d*\.?\d+)(ms|s)\s+cubic-bezier\(\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*\)\s*$/i;
185-
const match = css.match(re);
186-
187-
if (!match) {
188-
throw new Error(`Invalid timing string: ${css}`);
189-
}
190-
191-
const [, value, unit, x1, y1, x2, y2] = match as [
192-
string,
193-
string,
194-
string,
195-
string,
196-
string,
197-
string,
198-
string,
199-
];
200-
201-
// framer-motion expects duration in seconds
202-
const duration =
203-
unit.toLowerCase() === 'ms'
204-
? Number.parseFloat(value) / 1000
205-
: Number.parseFloat(value);
206-
207-
const ease: [number, number, number, number] = [
208-
Number.parseFloat(x1),
209-
Number.parseFloat(y1),
210-
Number.parseFloat(x2),
211-
Number.parseFloat(y2),
212-
];
213-
214-
return {duration, ease};
215-
}
216-
217185
function makeTargetState({property, state, easing}: TargetStateOptions) {
218186
const config = TARGET_CONFIGS[property as keyof typeof TARGET_CONFIGS] ?? TARGET_AXIS;
219187
return {
220188
[property]: config[easing][state],
221189
opacity: TARGET_OPACITY[easing][state],
222190
};
223191
}
224-
225-
function extractTokens<T extends Record<string, any>>(obj: T) {
226-
return Object.keys(obj) as Array<keyof T>;
227-
}

static/app/utils/theme/theme.tsx

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import type {CSSProperties} from 'react';
1111
import {css} from '@emotion/react';
1212
import color from 'color';
13+
import type {Transition} from 'framer-motion';
1314

1415
// palette generated via: https://gka.github.io/palettes/#colors=444674,69519A,E1567C,FB7D46,F2B712|steps=20|bez=1|coL=1
1516
const CHART_PALETTE = [
@@ -252,22 +253,71 @@ const generateTokens = (colors: Colors) => ({
252253
},
253254
});
254255

255-
const generateMotion = () => {
256-
return {
257-
smooth: withDuration('cubic-bezier(0.72, 0, 0.16, 1)'),
258-
snap: withDuration('cubic-bezier(0.8, -0.4, 0.5, 1)'),
259-
enter: withDuration('cubic-bezier(0.24, 1, 0.32, 1)'),
260-
exit: withDuration('cubic-bezier(0.64, 0, 0.8, 0)'),
256+
type MotionName = 'smooth' | 'snap' | 'enter' | 'exit';
257+
type MotionDuration = 'fast' | 'moderate' | 'slow';
258+
259+
type MotionDefinition = Record<MotionDuration, string>;
260+
261+
const motionDurations: Record<MotionDuration, number> = {
262+
fast: 120,
263+
moderate: 160,
264+
slow: 240,
265+
};
266+
267+
const motionCurves: Record<MotionName, [number, number, number, number]> = {
268+
smooth: [0.72, 0, 0.16, 1],
269+
snap: [0.8, -0.4, 0.5, 1],
270+
enter: [0.24, 1, 0.32, 1],
271+
exit: [0.64, 0, 0.8, 0],
272+
};
273+
274+
const withDuration = (
275+
durations: Record<MotionDuration, number>,
276+
easing: [number, number, number, number]
277+
): [MotionDefinition, Record<MotionDuration, Transition>] => {
278+
const motion: MotionDefinition = {
279+
fast: `${durations.fast}ms cubic-bezier(${easing.join(', ')})`,
280+
moderate: `${durations.moderate}ms cubic-bezier(${easing.join(', ')})`,
281+
slow: `${durations.slow}ms cubic-bezier(${easing.join(', ')})`,
261282
};
283+
284+
const framerMotion: Record<MotionDuration, Transition> = {
285+
fast: {
286+
duration: durations.fast / 1000,
287+
ease: easing,
288+
},
289+
moderate: {
290+
duration: durations.moderate / 1000,
291+
ease: easing,
292+
},
293+
slow: {
294+
duration: durations.slow / 1000,
295+
ease: easing,
296+
},
297+
};
298+
299+
return [motion, framerMotion];
262300
};
263301

264-
const withDuration = (easing: string) => {
302+
function generateMotion() {
303+
const [smoothMotion, smoothFramer] = withDuration(motionDurations, motionCurves.smooth);
304+
const [snapMotion, snapFramer] = withDuration(motionDurations, motionCurves.snap);
305+
const [enterMotion, enterFramer] = withDuration(motionDurations, motionCurves.enter);
306+
const [exitMotion, exitFramer] = withDuration(motionDurations, motionCurves.exit);
307+
265308
return {
266-
fast: `120ms ${easing}`,
267-
moderate: `160ms ${easing}`,
268-
slow: `240ms ${easing}`,
309+
smooth: smoothMotion,
310+
snap: snapMotion,
311+
enter: enterMotion,
312+
exit: exitMotion,
313+
framer: {
314+
smooth: smoothFramer,
315+
snap: snapFramer,
316+
enter: enterFramer,
317+
exit: exitFramer,
318+
},
269319
};
270-
};
320+
}
271321

272322
const generateThemeAliases = (colors: Colors) => ({
273323
/**

0 commit comments

Comments
 (0)