Skip to content

Commit 59d3f6e

Browse files
gggritsoandrewshie-sentry
authored andcommitted
feat(motion): Add "spring" design token (#102982)
Adds a new motion type called "spring". Unlike the current motion tokens, this one is physics-based, so its definition is not as a Bezier curve, but as a Framer Motion animation. It's mostly meant to be used with `motion.div` element, _however_ I'm also generating a CSS `linear()` function, so it can be used in CSS. Immediately uses the token in the place where I extracted it from, which is the `SlideOverPanel` component.
1 parent 78de2bc commit 59d3f6e

File tree

4 files changed

+90
-22
lines changed

4 files changed

+90
-22
lines changed

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,13 @@ function AnimatedComponent() {
8383

8484
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.
8585

86-
| **Name** | **Description** | **Value** |
87-
| -------- | -------------------------------------------------------------------------- | --------------------------------- |
88-
| `smooth` | similar to `ease-in-out`, natural acceleration and deceleration | `cubic-bezier(0.72, 0, 0.16, 1)` |
89-
| `snap` | an expressive snap, with slight anticipation and overshoot before settling | `cubic-bezier(0.8, -0.4, 0.5, 1)` |
90-
| `enter` | similar to `ease-out`, starts fast and slows into place smoothly | `cubic-bezier(0.24, 1, 0.32, 1)` |
91-
| `exit` | similar to `ease-in`, starts slowly and accelerates away quickly | `cubic-bezier(0.64, 0, 0.8, 0)` |
86+
| **Name** | **Description** | **Value** |
87+
| -------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
88+
| `smooth` | similar to `ease-in-out`, natural acceleration and deceleration | `cubic-bezier(0.72, 0, 0.16, 1)` |
89+
| `snap` | an expressive snap, with slight anticipation and overshoot before settling | `cubic-bezier(0.8, -0.4, 0.5, 1)` |
90+
| `spring` | a bouncy spring with some overshoot | `linear(0, 0.4005, 0.8613, 1.0429, 1.0528, 1.0214, 1.0015, 0.9965, 0.9977, 0.9994, 1.0001, 1)` |
91+
| `enter` | similar to `ease-out`, starts fast and slows into place smoothly | `cubic-bezier(0.24, 1, 0.32, 1)` |
92+
| `exit` | similar to `ease-in`, starts slowly and accelerates away quickly | `cubic-bezier(0.64, 0, 0.8, 0)` |
9293

9394
## Duration
9495

static/app/components/slideOverPanel.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {useEffect} from 'react';
22
import isPropValid from '@emotion/is-prop-valid';
3-
import {css} from '@emotion/react';
3+
import {css, useTheme} from '@emotion/react';
44
import styled from '@emotion/styled';
55
import {motion, type Transition} from 'framer-motion';
66

@@ -49,6 +49,8 @@ function SlideOverPanel({
4949
panelWidth,
5050
ref,
5151
}: SlideOverPanelProps) {
52+
const theme = useTheme();
53+
5254
useEffect(() => {
5355
if (!collapsed && onOpen) {
5456
onOpen();
@@ -69,9 +71,7 @@ function SlideOverPanel({
6971
exit={collapsedStyle}
7072
slidePosition={slidePosition}
7173
transition={{
72-
type: 'spring',
73-
stiffness: 1000,
74-
damping: 50,
74+
...theme.motion.framer.spring.moderate,
7575
...transitionProps,
7676
}}
7777
role="complementary"

static/app/stories/playground/motion.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ export function MotionPlayground() {
4444
<Grid columns="160px 192px" gap="lg" align="center" justify="center">
4545
<Control label="Easing">
4646
<CompactSelect
47-
options={(['smooth', 'snap', 'enter', 'exit'] as const).map(value => ({
48-
value,
49-
label: value,
50-
}))}
47+
options={(['smooth', 'snap', 'spring', 'enter', 'exit'] as const).map(
48+
value => ({
49+
value,
50+
label: value,
51+
})
52+
)}
5153
value={easing}
5254
onChange={opt => setEasing(opt.value)}
5355
/>
@@ -156,25 +158,29 @@ interface TargetStateOptions {
156158
const TARGET_OPACITY: TargetConfig = {
157159
smooth: {start: 1, end: 1},
158160
snap: {start: 1, end: 1},
161+
spring: {start: 1, end: 1},
159162
enter: {start: 0, end: 1},
160163
exit: {start: 1, end: 0},
161164
};
162165
const TARGET_AXIS: TargetConfig = {
163166
smooth: {start: -16, end: 16},
164167
snap: {start: -16, end: 16},
168+
spring: {start: -16, end: 16},
165169
enter: {start: -16, end: 0},
166170
exit: {start: 0, end: 16},
167171
};
168172
const TARGET_CONFIGS: Record<string, TargetConfig> = {
169173
rotate: {
170174
smooth: {start: 0, end: 90},
171175
snap: {start: 0, end: 90},
176+
spring: {start: 0, end: 90},
172177
enter: {start: -90, end: 0},
173178
exit: {start: 0, end: 90},
174179
},
175180
scale: {
176181
smooth: {start: 1, end: 1.125},
177182
snap: {start: 1, end: 1.125},
183+
spring: {start: 1, end: 1.125},
178184
enter: {start: 1.125, end: 1},
179185
exit: {start: 1, end: 0.8},
180186
},

static/app/utils/theme/theme.tsx

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +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';
13+
import {spring, type Transition} from 'framer-motion';
1414

1515
// palette generated via: https://gka.github.io/palettes/#colors=444674,69519A,E1567C,FB7D46,F2B712|steps=20|bez=1|coL=1
1616
const CHART_PALETTE = [
@@ -253,7 +253,10 @@ const generateTokens = (colors: Colors) => ({
253253
},
254254
});
255255

256-
type MotionName = 'smooth' | 'snap' | 'enter' | 'exit';
256+
type SimpleMotionName = 'smooth' | 'snap' | 'enter' | 'exit';
257+
258+
type PhysicsMotionName = 'spring';
259+
257260
type MotionDuration = 'fast' | 'moderate' | 'slow';
258261

259262
type MotionDefinition = Record<MotionDuration, string>;
@@ -264,14 +267,14 @@ const motionDurations: Record<MotionDuration, number> = {
264267
slow: 240,
265268
};
266269

267-
const motionCurves: Record<MotionName, [number, number, number, number]> = {
270+
const motionCurves: Record<SimpleMotionName, [number, number, number, number]> = {
268271
smooth: [0.72, 0, 0.16, 1],
269272
snap: [0.8, -0.4, 0.5, 1],
270273
enter: [0.24, 1, 0.32, 1],
271274
exit: [0.64, 0, 0.8, 0],
272275
};
273276

274-
const withDuration = (
277+
const motionCurveWithDuration = (
275278
durations: Record<MotionDuration, number>,
276279
easing: [number, number, number, number]
277280
): [MotionDefinition, Record<MotionDuration, Transition>] => {
@@ -299,22 +302,80 @@ const withDuration = (
299302
return [motion, framerMotion];
300303
};
301304

305+
const motionTransitions: Record<PhysicsMotionName, Record<MotionDuration, Transition>> = {
306+
spring: {
307+
fast: {
308+
type: 'spring',
309+
stiffness: 1400,
310+
damping: 50,
311+
},
312+
moderate: {
313+
type: 'spring',
314+
stiffness: 1000,
315+
damping: 50,
316+
},
317+
slow: {
318+
type: 'spring',
319+
stiffness: 600,
320+
damping: 50,
321+
},
322+
},
323+
};
324+
325+
const motionTransitionWithDuration = (
326+
transitionDefinitions: Record<MotionDuration, Transition>
327+
): [MotionDefinition, Record<MotionDuration, Transition>] => {
328+
const motion = {
329+
fast: `${spring({
330+
keyframes: [0, 1],
331+
...transitionDefinitions.fast,
332+
})}`,
333+
moderate: `${spring({
334+
keyframes: [0, 1],
335+
...transitionDefinitions.moderate,
336+
})}`,
337+
slow: `${spring({
338+
keyframes: [0, 1],
339+
...transitionDefinitions.slow,
340+
})}`,
341+
};
342+
343+
return [motion, transitionDefinitions];
344+
};
345+
302346
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);
347+
const [smoothMotion, smoothFramer] = motionCurveWithDuration(
348+
motionDurations,
349+
motionCurves.smooth
350+
);
351+
const [snapMotion, snapFramer] = motionCurveWithDuration(
352+
motionDurations,
353+
motionCurves.snap
354+
);
355+
const [enterMotion, enterFramer] = motionCurveWithDuration(
356+
motionDurations,
357+
motionCurves.enter
358+
);
359+
const [exitMotion, exitFramer] = motionCurveWithDuration(
360+
motionDurations,
361+
motionCurves.exit
362+
);
363+
const [springMotion, springFramer] = motionTransitionWithDuration(
364+
motionTransitions.spring
365+
);
307366

308367
return {
309368
smooth: smoothMotion,
310369
snap: snapMotion,
311370
enter: enterMotion,
312371
exit: exitMotion,
372+
spring: springMotion,
313373
framer: {
314374
smooth: smoothFramer,
315375
snap: snapFramer,
316376
enter: enterFramer,
317377
exit: exitFramer,
378+
spring: springFramer,
318379
},
319380
};
320381
}

0 commit comments

Comments
 (0)