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, };