Reduced motion preferences
Respect the user's reduced motion preference in a custom shape with usePrefersReducedMotion.
import {
Geometry2d,
HTMLContainer,
RecordProps,
Rectangle2d,
ShapeUtil,
T,
TLComponents,
TLShape,
Tldraw,
TldrawUiButton,
useEditor,
usePrefersReducedMotion,
} from 'tldraw'
import 'tldraw/tldraw.css'
import './reduced-motion.css'
const PULSE_SHAPE_TYPE = 'pulse-shape'
// [1]
declare module 'tldraw' {
export interface TLGlobalShapePropsMap {
[PULSE_SHAPE_TYPE]: { w: number; h: number }
}
}
type PulseShape = TLShape<typeof PULSE_SHAPE_TYPE>
// [2]
function PulseShapeComponent() {
const prefersReducedMotion = usePrefersReducedMotion()
return (
<HTMLContainer className="pulse-shape">
<div className="pulse-shape__content">
<div className={prefersReducedMotion ? 'pulse-indicator--static' : 'pulse-indicator'} />
<div className="pulse-shape__label">
{prefersReducedMotion ? 'Static mode' : 'Animated mode'}
</div>
</div>
</HTMLContainer>
)
}
// [3]
export class PulseShapeUtil extends ShapeUtil<PulseShape> {
static override type = PULSE_SHAPE_TYPE
static override props: RecordProps<PulseShape> = {
w: T.number,
h: T.number,
}
getDefaultProps(): PulseShape['props'] {
return { w: 200, h: 200 }
}
getGeometry(shape: PulseShape): Geometry2d {
return new Rectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: true,
})
}
component() {
return <PulseShapeComponent />
}
getIndicatorPath(shape: PulseShape) {
const path = new Path2D()
path.rect(0, 0, shape.props.w, shape.props.h)
return path
}
}
// [4]
function MotionToggle() {
const editor = useEditor()
const prefersReducedMotion = usePrefersReducedMotion()
const toggleMotion = () => {
const currentSpeed = editor.user.getAnimationSpeed()
editor.user.updateUserPreferences({
animationSpeed: currentSpeed === 0 ? 1 : 0,
})
}
return (
<div className="tlui-menu motion-toggle">
<span className="motion-toggle__label">
Motion: {prefersReducedMotion ? 'Reduced' : 'Normal'}
</span>
<TldrawUiButton type="primary" onClick={toggleMotion}>
Toggle
</TldrawUiButton>
</div>
)
}
const components: TLComponents = {
TopPanel: MotionToggle,
}
const shapeUtils = [PulseShapeUtil]
export default function ReducedMotionExample() {
return (
<div className="tldraw__editor">
<Tldraw
shapeUtils={shapeUtils}
components={components}
onMount={(editor) => {
editor.createShape({ type: PULSE_SHAPE_TYPE, x: 200, y: 200 })
editor.createShape({ type: PULSE_SHAPE_TYPE, x: 450, y: 200 })
editor.createShape({ type: PULSE_SHAPE_TYPE, x: 325, y: 450 })
}}
/>
</div>
)
}
/*
[1]
Registering the props in TLGlobalShapePropsMap is what makes `editor.createShape({ type })`
type-check and gives `TLShape<'pulse-shape'>` its props type.
[2]
`usePrefersReducedMotion()` returns true when the user's tldraw preference has `animationSpeed`
set to 0, or (if no preference is set) when the OS reports `prefers-reduced-motion: reduce`. It
is a hook, so the shape's rendering lives in a React component that `component()` returns; the
component re-renders when the preference changes and swaps the CSS class from the pulsing
indicator to the static one.
[3]
The util itself is minimal: geometry, indicator, and the component above.
[4]
The toggle flips the tldraw preference with `editor.user.updateUserPreferences()`. Setting
`animationSpeed: 0` is what tldraw's own "reduce motion" preference does, so the hook and every
shape using it respond the same way they would to the built-in setting.
*/
usePrefersReducedMotion() returns true when the user has turned on tldraw's "reduce motion" preference (animationSpeed: 0 in user preferences) or, if no tldraw preference is set, when the operating system reports prefers-reduced-motion: reduce. Use it inside a shape's React component to swap animations for static alternatives.
The pulse shapes here animate normally and fall back to a still gray circle when reduced motion is preferred. The "Toggle" button in the top panel flips the tldraw preference with editor.user.updateUserPreferences({ animationSpeed }), so you can watch every shape switch at once. The same setting is available to users under Preferences > Accessibility > Reduce motion in the main menu.
Reduced motion matters for users with vestibular disorders and motion sensitivity, and honoring it is cheap once the check is in one place.