From 3cac39b6cb857ddbcb4e9ac59b155a341db2a55f Mon Sep 17 00:00:00 2001
From: George Gritsouk <989898+gggritso@users.noreply.github.com>
Date: Thu, 30 Oct 2025 13:43:39 -0400
Subject: [PATCH 1/6] Animate using transforms
`x` and `y` coordinates are animated using `requestAnimationFrame`
---
static/app/components/slideOverPanel.tsx | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/static/app/components/slideOverPanel.tsx b/static/app/components/slideOverPanel.tsx
index 24055838656ddd..22f66989083899 100644
--- a/static/app/components/slideOverPanel.tsx
+++ b/static/app/components/slideOverPanel.tsx
@@ -11,15 +11,15 @@ const LEFT_SIDE_PANEL_WIDTH = '40vw';
const PANEL_HEIGHT = '50vh';
const OPEN_STYLES = {
- bottom: {opacity: 1, x: 0, y: 0},
- right: {opacity: 1, x: 0, y: 0},
- left: {opacity: 1, x: 0, y: 0},
+ bottom: {transform: 'translateX(0) translateY(0)', opacity: 1},
+ right: {transform: 'translateX(0) translateY(0)', opacity: 1},
+ left: {transform: 'translateX(0) translateY(0)', opacity: 1},
};
const COLLAPSED_STYLES = {
- bottom: {opacity: 0, x: 0, y: PANEL_HEIGHT},
- right: {opacity: 0, x: PANEL_WIDTH, y: 0},
- left: {opacity: 0, x: '-100%', y: 0},
+ bottom: {transform: `translateX(0) translateY(${PANEL_HEIGHT})`, opacity: 0},
+ right: {transform: `translateX(${PANEL_WIDTH}) translateY(0)`, opacity: 0},
+ left: {transform: `translateX(-${PANEL_WIDTH}) translateY(0)`, opacity: 0},
};
type SlideOverPanelProps = {
From 30a407f872061ed18a91359bc6cd2e81c482c697 Mon Sep 17 00:00:00 2001
From: George Gritsouk <989898+gggritso@users.noreply.github.com>
Date: Thu, 30 Oct 2025 14:14:37 -0400
Subject: [PATCH 2/6] Split out motion token raw values
This makes it possible to use them in Framer Motion components, which
need numeric values.
---
.../core/principles/motion/motion.mdx | 15 +++++++
static/app/utils/theme/theme.tsx | 42 +++++++++++++++----
2 files changed, 48 insertions(+), 9 deletions(-)
diff --git a/static/app/components/core/principles/motion/motion.mdx b/static/app/components/core/principles/motion/motion.mdx
index 39599fe6835c27..3d7f2d410576f4 100644
--- a/static/app/components/core/principles/motion/motion.mdx
+++ b/static/app/components/core/principles/motion/motion.mdx
@@ -59,6 +59,21 @@ const theme = useTheme();
theme.motion.smooth.moderate;
```
+If you need to use the tokens with [Framer Motion](https://motion.dev) you can access the Bézier curve control points values and durations directly.
+
+```jsx
+const theme = useTheme();
+
+;
+```
+
## Easing
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.
diff --git a/static/app/utils/theme/theme.tsx b/static/app/utils/theme/theme.tsx
index ac5ba0b9fa966c..d26adeb677d66a 100644
--- a/static/app/utils/theme/theme.tsx
+++ b/static/app/utils/theme/theme.tsx
@@ -252,20 +252,42 @@ const generateTokens = (colors: Colors) => ({
},
});
-const generateMotion = () => {
+type Curve = 'smooth' | 'snap' | 'enter' | 'exit';
+type ControlPoints = [number, number, number, number];
+
+const BEZIER_CONTROL_POINTS: Record = {
+ smooth: [0.72, 0, 0.16, 1],
+ snap: [0.8, -0.4, 0.5, 1],
+ enter: [0.24, 1, 0.32, 1],
+ exit: [0.64, 0, 0.8, 0],
+};
+
+function formatBezierCurve(points: ControlPoints): string {
+ return `cubic-bezier(${points.join(', ')})`;
+}
+
+type AnimationDuration = 'fast' | 'moderate' | 'slow';
+
+const MOTION_DURATIONS: Record = {
+ fast: 0.12,
+ moderate: 0.16,
+ slow: 0.24,
+};
+
+const withDuration = (easing: string) => {
return {
- smooth: withDuration('cubic-bezier(0.72, 0, 0.16, 1)'),
- snap: withDuration('cubic-bezier(0.8, -0.4, 0.5, 1)'),
- enter: withDuration('cubic-bezier(0.24, 1, 0.32, 1)'),
- exit: withDuration('cubic-bezier(0.64, 0, 0.8, 0)'),
+ fast: `${MOTION_DURATIONS.fast}s ${easing}`,
+ moderate: `${MOTION_DURATIONS.moderate}s ${easing}`,
+ slow: `${MOTION_DURATIONS.slow}s ${easing}`,
};
};
-const withDuration = (easing: string) => {
+const generateMotion = () => {
return {
- fast: `120ms ${easing}`,
- moderate: `160ms ${easing}`,
- slow: `240ms ${easing}`,
+ smooth: withDuration(formatBezierCurve(BEZIER_CONTROL_POINTS.smooth)),
+ snap: withDuration(formatBezierCurve(BEZIER_CONTROL_POINTS.snap)),
+ enter: withDuration(formatBezierCurve(BEZIER_CONTROL_POINTS.enter)),
+ exit: withDuration(formatBezierCurve(BEZIER_CONTROL_POINTS.exit)),
};
};
@@ -1152,6 +1174,8 @@ const commonTheme = {
space,
motion: generateMotion(),
+ motionControlPoints: BEZIER_CONTROL_POINTS,
+ motionDurations: MOTION_DURATIONS,
// Icons
iconSizes,
From 7a7169d1050fa8cba6e67de5de46de776f87a32f Mon Sep 17 00:00:00 2001
From: George Gritsouk <989898+gggritso@users.noreply.github.com>
Date: Thu, 30 Oct 2025 14:15:12 -0400
Subject: [PATCH 3/6] Use new design tokens for panel animations
---
static/app/components/slideOverPanel.tsx | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/static/app/components/slideOverPanel.tsx b/static/app/components/slideOverPanel.tsx
index 22f66989083899..bf85fd2295cc8c 100644
--- a/static/app/components/slideOverPanel.tsx
+++ b/static/app/components/slideOverPanel.tsx
@@ -1,6 +1,6 @@
import {useEffect} from 'react';
import isPropValid from '@emotion/is-prop-valid';
-import {css} from '@emotion/react';
+import {css, useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import {motion, type Transition} from 'framer-motion';
@@ -49,6 +49,8 @@ function SlideOverPanel({
panelWidth,
ref,
}: SlideOverPanelProps) {
+ const theme = useTheme();
+
useEffect(() => {
if (!collapsed && onOpen) {
onOpen();
@@ -69,9 +71,9 @@ function SlideOverPanel({
exit={collapsedStyle}
slidePosition={slidePosition}
transition={{
- type: 'spring',
- stiffness: 1000,
- damping: 50,
+ type: 'tween',
+ ease: theme.motionControlPoints.enter,
+ duration: theme.motionDurations.slow,
...transitionProps,
}}
role="complementary"
From 0d8f123faa3debb24a2d9bdb6f68d3289120b197 Mon Sep 17 00:00:00 2001
From: George Gritsouk <989898+gggritso@users.noreply.github.com>
Date: Thu, 30 Oct 2025 14:22:21 -0400
Subject: [PATCH 4/6] Use design tokens in Widget Builder animations
---
.../components/common/animationSettings.tsx | 6 ------
.../widgetBuilder/components/newWidgetBuilder.tsx | 5 +++--
.../components/widgetBuilderSlideout.tsx | 5 +++--
.../hooks/useWidgetBuilderAnimationSettings.tsx | 12 ++++++++++++
4 files changed, 18 insertions(+), 10 deletions(-)
delete mode 100644 static/app/views/dashboards/widgetBuilder/components/common/animationSettings.tsx
create mode 100644 static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderAnimationSettings.tsx
diff --git a/static/app/views/dashboards/widgetBuilder/components/common/animationSettings.tsx b/static/app/views/dashboards/widgetBuilder/components/common/animationSettings.tsx
deleted file mode 100644
index 7d38656bec75ec..00000000000000
--- a/static/app/views/dashboards/widgetBuilder/components/common/animationSettings.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import type {MotionNodeAnimationOptions} from 'framer-motion';
-
-export const animationTransitionSettings: MotionNodeAnimationOptions['transition'] = {
- type: 'tween',
- duration: 0.5,
-};
diff --git a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
index ad699b4ad8a300..243636f26d8e33 100644
--- a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
+++ b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
@@ -25,7 +25,6 @@ import {
type DashboardFilters,
type Widget,
} from 'sentry/views/dashboards/types';
-import {animationTransitionSettings} from 'sentry/views/dashboards/widgetBuilder/components/common/animationSettings';
import {
DEFAULT_WIDGET_DRAG_POSITIONING,
DRAGGABLE_PREVIEW_HEIGHT_PX,
@@ -43,6 +42,7 @@ import {
useWidgetBuilderContext,
WidgetBuilderProvider,
} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
+import {useWidgetBuilderTransitionSettings} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderAnimationSettings';
import {DashboardsMEPProvider} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext';
import {TraceItemAttributeProvider} from 'sentry/views/explore/contexts/traceItemAttributeContext';
import {isLogsEnabled} from 'sentry/views/explore/logs/isLogsEnabled';
@@ -269,6 +269,7 @@ export function WidgetPreviewContainer({
const organization = useOrganization();
const location = useLocation();
const theme = useTheme();
+ const transitionSettings = useWidgetBuilderTransitionSettings();
const isSmallScreen = useMedia(`(max-width: ${theme.breakpoints.sm})`);
// if small screen and draggable, enable dragging
const isDragEnabled = isSmallScreen && isDraggable;
@@ -334,7 +335,7 @@ export function WidgetPreviewContainer({
initial: {opacity: 0, x: '100%', y: 0},
animate: {opacity: 1, x: 0, y: 0},
exit: {opacity: 0, x: '100%', y: 0},
- transition: animationTransitionSettings,
+ transition: transitionSettings,
};
return (
diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx
index cae3e04b965f29..0664ca0fa5ec51 100644
--- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx
+++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx
@@ -27,7 +27,6 @@ import {
type DashboardFilters,
type Widget,
} from 'sentry/views/dashboards/types';
-import {animationTransitionSettings} from 'sentry/views/dashboards/widgetBuilder/components/common/animationSettings';
import WidgetBuilderDatasetSelector from 'sentry/views/dashboards/widgetBuilder/components/datasetSelector';
import WidgetBuilderFilterBar from 'sentry/views/dashboards/widgetBuilder/components/filtersBar';
import WidgetBuilderGroupBySelector from 'sentry/views/dashboards/widgetBuilder/components/groupBySelector';
@@ -49,6 +48,7 @@ import useDashboardWidgetSource from 'sentry/views/dashboards/widgetBuilder/hook
import {useDisableTransactionWidget} from 'sentry/views/dashboards/widgetBuilder/hooks/useDisableTransactionWidget';
import useIsEditingWidget from 'sentry/views/dashboards/widgetBuilder/hooks/useIsEditingWidget';
import {useSegmentSpanWidgetState} from 'sentry/views/dashboards/widgetBuilder/hooks/useSegmentSpanWidgetState';
+import {useWidgetBuilderTransitionSettings} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderAnimationSettings';
import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget';
import {convertWidgetToBuilderStateParams} from 'sentry/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams';
import {getTopNConvertedDefaultWidgets} from 'sentry/views/dashboards/widgetLibrary/data';
@@ -90,6 +90,7 @@ function WidgetBuilderSlideout({
const [error, setError] = useState>({});
const theme = useTheme();
const isEditing = useIsEditingWidget();
+ const transitionSettings = useWidgetBuilderTransitionSettings();
const source = useDashboardWidgetSource();
const {cacheBuilderState} = useCacheBuilderState();
const {setSegmentSpanBuilderState} = useSegmentSpanWidgetState();
@@ -208,7 +209,7 @@ function WidgetBuilderSlideout({
collapsed={!isOpen}
slidePosition="left"
data-test-id="widget-slideout"
- transitionProps={animationTransitionSettings}
+ transitionProps={transitionSettings}
>
diff --git a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderAnimationSettings.tsx b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderAnimationSettings.tsx
new file mode 100644
index 00000000000000..c927aa014ba7fb
--- /dev/null
+++ b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderAnimationSettings.tsx
@@ -0,0 +1,12 @@
+import {useTheme} from '@emotion/react';
+import type {Transition} from 'framer-motion';
+
+export function useWidgetBuilderTransitionSettings(): Transition {
+ const theme = useTheme();
+
+ return {
+ type: 'tween',
+ ease: theme.motionControlPoints.snap,
+ duration: 0.5, // TODO: Introduce a slower value
+ };
+}
From 524c51a0883ae0d1899bac112f80a8138e22f4d6 Mon Sep 17 00:00:00 2001
From: George Gritsouk <989898+gggritso@users.noreply.github.com>
Date: Thu, 30 Oct 2025 14:24:28 -0400
Subject: [PATCH 5/6] Animate using `transform`
More better performance!
---
.../widgetBuilder/components/newWidgetBuilder.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
index 243636f26d8e33..4f420b6b99dfb8 100644
--- a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
+++ b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
@@ -332,9 +332,9 @@ export function WidgetPreviewContainer({
};
const animatedProps: MotionNodeAnimationOptions = {
- initial: {opacity: 0, x: '100%', y: 0},
- animate: {opacity: 1, x: 0, y: 0},
- exit: {opacity: 0, x: '100%', y: 0},
+ initial: {opacity: 0, transform: 'translateX(100%) translateY(0)'},
+ animate: {opacity: 1, transform: 'translateX(0) translateY(0)'},
+ exit: {opacity: 0, transform: 'translateX(100%) translateY(0)'},
transition: transitionSettings,
};
From 228fcde3dcaa55ffe4d0049edd7ebe5e284d54cf Mon Sep 17 00:00:00 2001
From: George Gritsouk <989898+gggritso@users.noreply.github.com>
Date: Thu, 30 Oct 2025 15:38:49 -0400
Subject: [PATCH 6/6] Unroll transform changes
---
static/app/components/slideOverPanel.tsx | 12 ++++++------
.../widgetBuilder/components/newWidgetBuilder.tsx | 6 +++---
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/static/app/components/slideOverPanel.tsx b/static/app/components/slideOverPanel.tsx
index bf85fd2295cc8c..2c9253334d01a7 100644
--- a/static/app/components/slideOverPanel.tsx
+++ b/static/app/components/slideOverPanel.tsx
@@ -11,15 +11,15 @@ const LEFT_SIDE_PANEL_WIDTH = '40vw';
const PANEL_HEIGHT = '50vh';
const OPEN_STYLES = {
- bottom: {transform: 'translateX(0) translateY(0)', opacity: 1},
- right: {transform: 'translateX(0) translateY(0)', opacity: 1},
- left: {transform: 'translateX(0) translateY(0)', opacity: 1},
+ bottom: {opacity: 1, x: 0, y: 0},
+ right: {opacity: 1, x: 0, y: 0},
+ left: {opacity: 1, x: 0, y: 0},
};
const COLLAPSED_STYLES = {
- bottom: {transform: `translateX(0) translateY(${PANEL_HEIGHT})`, opacity: 0},
- right: {transform: `translateX(${PANEL_WIDTH}) translateY(0)`, opacity: 0},
- left: {transform: `translateX(-${PANEL_WIDTH}) translateY(0)`, opacity: 0},
+ bottom: {opacity: 0, x: 0, y: PANEL_HEIGHT},
+ right: {opacity: 0, x: PANEL_WIDTH, y: 0},
+ left: {opacity: 0, x: '-100%', y: 0},
};
type SlideOverPanelProps = {
diff --git a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
index 4f420b6b99dfb8..243636f26d8e33 100644
--- a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
+++ b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
@@ -332,9 +332,9 @@ export function WidgetPreviewContainer({
};
const animatedProps: MotionNodeAnimationOptions = {
- initial: {opacity: 0, transform: 'translateX(100%) translateY(0)'},
- animate: {opacity: 1, transform: 'translateX(0) translateY(0)'},
- exit: {opacity: 0, transform: 'translateX(100%) translateY(0)'},
+ initial: {opacity: 0, x: '100%', y: 0},
+ animate: {opacity: 1, x: 0, y: 0},
+ exit: {opacity: 0, x: '100%', y: 0},
transition: transitionSettings,
};