Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Copy link
Copy Markdown

@github-actions github-actions Bot Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Charts-DonutChart 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Charts-DonutChart.Dynamic - RTL.default.chromium.png 5570 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.chromium.png 957 Changed
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 607 Changed

"type": "patch",
"comment": "docs(motion): add motion system docs",
"packageName": "@fluentui/react-motion",
"email": "robertpenner@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "docs(motion): add motion components docs",
"packageName": "@fluentui/react-motion-components-preview",
"email": "robertpenner@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,93 @@
# @fluentui/react-motion-components-preview

**React Motion Components for [Fluent UI React](https://react.fluentui.dev/)**
**Pre-built Motion Components for [Fluent UI React](https://react.fluentui.dev/)**

These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release.
> ⚠️ **Preview Package**: These components are in beta and APIs may change before stable release.

Ready-to-use presence components for common UI animation patterns, built on top of `@fluentui/react-motion`.

## Components

| Component | Description |
| ------------ | ---------------------------------------------------------- |
| **Fade** | Opacity transitions for tooltips, notifications, overlays |
| **Scale** | Size animations for popovers, menus, emphasis |
| **Collapse** | Height/width expansion for accordions, expandable sections |
| **Slide** | Directional movement for drawers, panels, carousels |
| **Blur** | Focus/defocus effects for backgrounds, depth |
| **Rotate** | 3D rotation for card flips, reveals |
| **Stagger** | Choreography for sequential list animations |

Each component (except Blur and Rotate) comes with **Snappy** (150ms) and **Relaxed** (250ms) timing variants.

## Installation

```bash
npm install @fluentui/react-motion-components-preview @fluentui/react-motion
# or
yarn add @fluentui/react-motion-components-preview @fluentui/react-motion
```

## Quick Start

```tsx
import { Fade, Scale, Slide, Collapse } from '@fluentui/react-motion-components-preview';

// Simple fade
function Tooltip({ visible, children }) {
return (
<Fade visible={visible}>
{children}
</Fade>
);
}

// Slide from the right
function Drawer({ open, children }) {
return (
<Slide visible={open} outX="20px">
{children}
</Slide>
);
}

// Use timing variants
import { FadeSnappy, ScaleRelaxed } from '@fluentui/react-motion-components-preview';

<FadeSnappy visible={show}>Quick feedback</FadeSnappy>
<ScaleRelaxed visible={show}>Smooth entrance</ScaleRelaxed>
```

### The `.In` and `.Out` Pattern

Every presence component includes one-way sub-components:

```tsx
// One-way enter animation (plays on mount)
<Fade.In>
<div>Fades in once</div>
</Fade.In>

// One-way exit animation (plays on mount)
<Fade.Out>
<div>Fades out once</div>
</Fade.Out>
```

## Documentation

📚 **[Full documentation](https://react.fluentui.dev/?path=/docs/motion-components-preview-introduction--docs)**

- [Introduction](https://react.fluentui.dev/?path=/docs/motion-components-preview-introduction--docs) — Overview of all components
- [Fade](https://react.fluentui.dev/?path=/docs/motion-components-preview-fade--docs)
- [Scale](https://react.fluentui.dev/?path=/docs/motion-components-preview-scale--docs)
- [Collapse](https://react.fluentui.dev/?path=/docs/motion-components-preview-collapse--docs)
- [Slide](https://react.fluentui.dev/?path=/docs/motion-components-preview-slide--docs)
- [Blur](https://react.fluentui.dev/?path=/docs/motion-components-preview-blur--docs)
- [Rotate](https://react.fluentui.dev/?path=/docs/motion-components-preview-rotate--docs)
- [Stagger](https://react.fluentui.dev/?path=/docs/motion-choreography-preview-stagger--docs)
- [Motion Atoms](https://react.fluentui.dev/?path=/docs/motion-components-preview-atoms--docs) — Building blocks for custom components

## Related

- **[@fluentui/react-motion](https://www.npmjs.com/package/@fluentui/react-motion)** — Core motion APIs for creating custom animations
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as React from 'react';
import { makeStyles, tokens, Button, Select } from '@fluentui/react-components';
import { createMotionComponent, motionTokens } from '@fluentui/react-motion';
import { fadeAtom, scaleAtom, slideAtom, rotateAtom, blurAtom } from '@fluentui/react-motion-components-preview';

const useClasses = makeStyles({
container: {
display: 'flex',
flexDirection: 'column',
gap: '16px',
padding: '24px',
border: `1px solid ${tokens.colorNeutralStroke1}`,
borderRadius: tokens.borderRadiusMedium,
marginTop: '24px',
marginBottom: '24px',
},
controls: {
display: 'flex',
alignItems: 'center',
gap: '16px',
flexWrap: 'wrap',
},
demoArea: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '120px',
backgroundColor: tokens.colorNeutralBackground3,
borderRadius: tokens.borderRadiusSmall,
padding: '20px',
},
demoBox: {
width: '100px',
height: '80px',
backgroundColor: tokens.colorBrandBackground,
borderRadius: tokens.borderRadiusMedium,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: tokens.colorNeutralForegroundOnBrand,
fontWeight: tokens.fontWeightSemibold,
boxShadow: tokens.shadow8,
},
label: {
fontSize: tokens.fontSizeBase200,
color: tokens.colorNeutralForeground3,
},
});

type AtomType = 'fade' | 'scale' | 'slide' | 'rotate' | 'blur';

const createAtomMotion = (type: AtomType) => {
const duration = motionTokens.durationNormal;
const easing = motionTokens.curveDecelerateMid;

switch (type) {
case 'fade':
return createMotionComponent(fadeAtom({ direction: 'enter', duration, easing }));
case 'scale':
return createMotionComponent(scaleAtom({ direction: 'enter', duration, easing, outScale: 0.5 }));
case 'slide':
return createMotionComponent(slideAtom({ direction: 'enter', duration, easing, outY: '30px' }));
case 'rotate':
return createMotionComponent(
rotateAtom({ direction: 'enter', duration: 400, easing, axis: 'y', outAngle: -180 }),
);
case 'blur':
return createMotionComponent(blurAtom({ direction: 'enter', duration, easing, outRadius: '10px' }));
default:
return createMotionComponent(fadeAtom({ direction: 'enter', duration, easing }));
}
};

const atomLabels: Record<AtomType, string> = {
fade: 'fadeAtom',
scale: 'scaleAtom',
slide: 'slideAtom',
rotate: 'rotateAtom',
blur: 'blurAtom',
};

export const AtomsDemo: React.FC = () => {
const classes = useClasses();
const [atomType, setAtomType] = React.useState<AtomType>('fade');
const [key, setKey] = React.useState(0);

const MotionComponent = React.useMemo(() => createAtomMotion(atomType), [atomType]);

const handleAtomChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setAtomType(event.target.value as AtomType);
setKey(k => k + 1);
};

return (
<div className={classes.container}>
<div className={classes.controls}>
<Select value={atomType} onChange={handleAtomChange}>
<option value="fade">fadeAtom</option>
<option value="scale">scaleAtom</option>
<option value="slide">slideAtom</option>
<option value="rotate">rotateAtom</option>
<option value="blur">blurAtom</option>
</Select>
<Button onClick={() => setKey(k => k + 1)}>Replay</Button>
<span className={classes.label}>Select an atom to see its effect</span>
</div>
<div className={classes.demoArea}>
<MotionComponent key={key}>
<div className={classes.demoBox}>{atomLabels[atomType]}</div>
</MotionComponent>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import * as React from 'react';
import { makeStyles, tokens, Button } from '@fluentui/react-components';
import { createPresenceComponent, motionTokens } from '@fluentui/react-motion';
import { fadeAtom, scaleAtom, slideAtom } from '@fluentui/react-motion-components-preview';

const useClasses = makeStyles({
container: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '16px',
marginTop: '24px',
marginBottom: '24px',
},
panel: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '12px',
padding: '20px',
border: `1px solid ${tokens.colorNeutralStroke1}`,
borderRadius: tokens.borderRadiusMedium,
backgroundColor: tokens.colorNeutralBackground1,
},
title: {
margin: 0,
fontWeight: tokens.fontWeightSemibold,
fontSize: tokens.fontSizeBase300,
textAlign: 'center',
},
description: {
margin: 0,
fontSize: tokens.fontSizeBase200,
color: tokens.colorNeutralForeground3,
textAlign: 'center',
},
demoArea: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '80px',
},
demoBox: {
width: '80px',
height: '60px',
backgroundColor: tokens.colorBrandBackground,
borderRadius: tokens.borderRadiusSmall,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: tokens.colorNeutralForegroundOnBrand,
fontSize: tokens.fontSizeBase200,
fontWeight: tokens.fontWeightSemibold,
},
button: {
minWidth: 'auto',
padding: '4px 12px',
fontSize: tokens.fontSizeBase200,
},
});

// Custom "Pop" effect: fade + scale
const Pop = createPresenceComponent({
enter: [
fadeAtom({ direction: 'enter', duration: motionTokens.durationNormal }),
scaleAtom({ direction: 'enter', duration: motionTokens.durationNormal, outScale: 0.7 }),
],
exit: [
fadeAtom({ direction: 'exit', duration: motionTokens.durationFast }),
scaleAtom({ direction: 'exit', duration: motionTokens.durationFast, outScale: 0.7 }),
],
});

// Custom "FadeSlide" effect: fade + slide from bottom
const FadeSlide = createPresenceComponent({
enter: [
fadeAtom({ direction: 'enter', duration: motionTokens.durationNormal }),
slideAtom({ direction: 'enter', duration: motionTokens.durationNormal, outY: '20px' }),
],
exit: [
fadeAtom({ direction: 'exit', duration: motionTokens.durationFast }),
slideAtom({ direction: 'exit', duration: motionTokens.durationFast, outY: '20px' }),
],
});

// Custom "ScaleSlide" effect: scale + slide from left
const ScaleSlide = createPresenceComponent({
enter: [
scaleAtom({ direction: 'enter', duration: motionTokens.durationNormal, outScale: 0.8 }),
slideAtom({ direction: 'enter', duration: motionTokens.durationNormal, outX: '-30px' }),
],
exit: [
scaleAtom({ direction: 'exit', duration: motionTokens.durationFast, outScale: 0.8 }),
slideAtom({ direction: 'exit', duration: motionTokens.durationFast, outX: '-30px' }),
],
});

interface DemoPanelProps {
title: string;
description: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
PresenceComponent: any;
}

const DemoPanel: React.FC<DemoPanelProps> = ({ title, description, PresenceComponent }) => {
const classes = useClasses();
const [visible, setVisible] = React.useState(true);

return (
<div className={classes.panel}>
<h4 className={classes.title}>{title}</h4>
<p className={classes.description}>{description}</p>
<div className={classes.demoArea}>
<PresenceComponent visible={visible}>
<div className={classes.demoBox}>{title}</div>
</PresenceComponent>
</div>
<Button className={classes.button} size="small" onClick={() => setVisible(v => !v)}>
{visible ? 'Hide' : 'Show'}
</Button>
</div>
);
};

export const ComposingAtomsDemo: React.FC = () => {
const classes = useClasses();

return (
<div className={classes.container}>
<DemoPanel title="Pop" description="fade + scale" PresenceComponent={Pop} />
<DemoPanel title="FadeSlide" description="fade + slide" PresenceComponent={FadeSlide} />
<DemoPanel title="ScaleSlide" description="scale + slide" PresenceComponent={ScaleSlide} />
</div>
);
};
Loading
Loading