Skip to main content

gentleduck motion

WIP

Motion primitives and reduced-motion utilities used across the gentleduck ecosystem.

Philosophy

@gentleduck/motion is a small motion layer. It ships shared easing and duration tokens plus reduced-motion handling so transitions stay consistent across packages.

Installation


npm install @gentleduck/motion

npm install @gentleduck/motion

Usage

import '@gentleduck/motion/css'
import { MotionProvider, motionTransition, useDuckReducedMotion } from '@gentleduck/motion'
import '@gentleduck/motion/css'
import { MotionProvider, motionTransition, useDuckReducedMotion } from '@gentleduck/motion'
function App() {
  return (
    <MotionProvider>
      {/* All m.* components inside get consistent defaults */}
    </MotionProvider>
  )
}
function App() {
  return (
    <MotionProvider>
      {/* All m.* components inside get consistent defaults */}
    </MotionProvider>
  )
}

Motion Library Integration

@gentleduck/motion integrates with the motion library (v12+) for enter/exit transitions, layout animations, and gestures. The motion library is an optional peer dependency; install it when you need those features.


npm install motion

npm install motion

API Reference

MotionProvider

Wraps LazyMotion + MotionConfig with duck-ui defaults and reduced-motion support.

import { MotionProvider } from '@gentleduck/motion'
 
function App() {
  return (
    <MotionProvider exitTransition={{ duration: 0.32 }}>
      <m.div animate={{ opacity: 1 }} />
    </MotionProvider>
  )
}
import { MotionProvider } from '@gentleduck/motion'
 
function App() {
  return (
    <MotionProvider exitTransition={{ duration: 0.32 }}>
      <m.div animate={{ opacity: 1 }} />
    </MotionProvider>
  )
}
PropTypeDefaultDescription
childrenReact.ReactNodeContent to render inside the provider
transitionTransition{ duration: 0.15, ease: [0.4, 0, 0.2, 1] }Global default transition
enterTransitionTransitionOverride transition for enter animations only
exitTransitionTransitionOverride transition for exit animations only
reducedMotion'user' | 'always' | 'never''user'Reduced-motion strategy passed to MotionConfig
features() => Promise<FeatureBundle>loadDomAnimationLazyMotion feature loader
strictbooleanfalseEnable strict mode (warns on motion.* usage)

useMotionConfig

Access the IMotionConfigContextValue from inside a MotionProvider.

import { useMotionConfig } from '@gentleduck/motion'
 
function MyComponent() {
  const { exitTransition } = useMotionConfig()
  // ...
}
import { useMotionConfig } from '@gentleduck/motion'
 
function MyComponent() {
  const { exitTransition } = useMotionConfig()
  // ...
}

Returns IMotionConfigContextValue:

FieldTypeDescription
exitTransitionTransition | undefinedExit transition set on the nearest MotionProvider

IDuckMotion namespace

Type namespace for all motion-related types. Import as a type only.

import type { IDuckMotion } from '@gentleduck/motion'
 
const preset: IDuckMotion.IPreset = {
  initial: { opacity: 0 },
  animate: { opacity: 1 },
  exit: { opacity: 0 },
}
import type { IDuckMotion } from '@gentleduck/motion'
 
const preset: IDuckMotion.IPreset = {
  initial: { opacity: 0 },
  animate: { opacity: 1 },
  exit: { opacity: 0 },
}

Available types: IAnimationState, ITransitionConfig, IPreset, IPresetResult, IPresetOptions, IPresetName, IDirection.


Stagger Utilities

Helpers for staggered enter animations.

import { getStaggerDelay, createStagger, staggerChildren } from '@gentleduck/motion'
import { getStaggerDelay, createStagger, staggerChildren } from '@gentleduck/motion'

getStaggerDelay

Returns the stagger delay in seconds for a single item. Avoids allocating an array when only one delay is needed.

const delay = getStaggerDelay(index, 50, 100) // (index * 50ms + 100ms) / 1000
const delay = getStaggerDelay(index, 50, 100) // (index * 50ms + 100ms) / 1000
ParameterTypeDefaultDescription
indexnumberItem index
staggerMsnumberPer-item stagger in milliseconds
delayMsnumber0Initial delay before the first item in milliseconds

createStagger

Creates an array of { delay } objects (in seconds) for staggered animations.

const delays = createStagger(5, 50, 100)
// [{ delay: 0.1 }, { delay: 0.15 }, { delay: 0.2 }, { delay: 0.25 }, { delay: 0.3 }]
 
delays.map(({ delay }, i) => (
  <m.div key={i} animate={{ opacity: 1 }} transition={{ delay }}>
    Item {i}
  </m.div>
))
const delays = createStagger(5, 50, 100)
// [{ delay: 0.1 }, { delay: 0.15 }, { delay: 0.2 }, { delay: 0.25 }, { delay: 0.3 }]
 
delays.map(({ delay }, i) => (
  <m.div key={i} animate={{ opacity: 1 }} transition={{ delay }}>
    Item {i}
  </m.div>
))

staggerChildren

Returns a Transition config for motion's staggerChildren / delayChildren. Spread into a parent variant's transition field.

const containerVariants = {
  animate: {
    transition: staggerChildren(50, 100),
  },
}
 
<m.ul variants={containerVariants} animate='animate'>
  {items.map((item) => (
    <m.li key={item.id} variants={itemVariants}>
      {item.label}
    </m.li>
  ))}
</m.ul>
const containerVariants = {
  animate: {
    transition: staggerChildren(50, 100),
  },
}
 
<m.ul variants={containerVariants} animate='animate'>
  {items.map((item) => (
    <m.li key={item.id} variants={itemVariants}>
      {item.label}
    </m.li>
  ))}
</m.ul>

Transitions

Duration tokens

import { duckMotionDuration, duckMotionDurationMs } from '@gentleduck/motion'
 
// Seconds (for motion library)
duckMotionDuration.instant  // 0
duckMotionDuration.fast    // 0.15
duckMotionDuration.normal  // 0.2
duckMotionDuration.slow    // 0.3
 
// Milliseconds (for CSS)
duckMotionDurationMs.instant  // 0
duckMotionDurationMs.fast     // 150
duckMotionDurationMs.normal   // 200
duckMotionDurationMs.slow     // 300
import { duckMotionDuration, duckMotionDurationMs } from '@gentleduck/motion'
 
// Seconds (for motion library)
duckMotionDuration.instant  // 0
duckMotionDuration.fast    // 0.15
duckMotionDuration.normal  // 0.2
duckMotionDuration.slow    // 0.3
 
// Milliseconds (for CSS)
duckMotionDurationMs.instant  // 0
duckMotionDurationMs.fast     // 150
duckMotionDurationMs.normal   // 200
duckMotionDurationMs.slow     // 300

Easing tokens

import { duckMotionEasing, duckMotionEasingCss } from '@gentleduck/motion'
 
// Cubic-bezier arrays (for motion library)
duckMotionEasing.standard  // [0.4, 0, 0.2, 1]
duckMotionEasing.spring    // [1, 0.23995, 0, 1.65]
 
// CSS strings (for CSS transitions)
duckMotionEasingCss.standard  // 'cubic-bezier(0.4, 0, 0.2, 1)'
duckMotionEasingCss.spring    // 'cubic-bezier(1, 0.23995, 0, 1.65)'
import { duckMotionEasing, duckMotionEasingCss } from '@gentleduck/motion'
 
// Cubic-bezier arrays (for motion library)
duckMotionEasing.standard  // [0.4, 0, 0.2, 1]
duckMotionEasing.spring    // [1, 0.23995, 0, 1.65]
 
// CSS strings (for CSS transitions)
duckMotionEasingCss.standard  // 'cubic-bezier(0.4, 0, 0.2, 1)'
duckMotionEasingCss.spring    // 'cubic-bezier(1, 0.23995, 0, 1.65)'

Tween presets

Ready-to-use tween Transition objects. Import individually:

import {
  tweenFast,
  tweenNormal,
  tweenSlow,
  tweenMicro,
  tweenInstant,
  tweenExit,
  tweenExpand,
  tweenShake,
} from '@gentleduck/motion'
import {
  tweenFast,
  tweenNormal,
  tweenSlow,
  tweenMicro,
  tweenInstant,
  tweenExit,
  tweenExpand,
  tweenShake,
} from '@gentleduck/motion'
NameDurationEaseUse case
tweenInstant0Immediate state changes
tweenMicro100msease-outTooltips, micro-interactions
tweenFast150msstandardHover states, toggles, tooltips
tweenNormal200msstandardOverlays, content reveals
tweenSlow300msstandardLarge layout changes, page transitions
tweenExit200msaggressiveClosing menus and dialogs
tweenExpand250msexpo-outAccordion, collapsible, height reveals
tweenShake400msError shake feedback

Spring presets

Ready-to-use spring Transition objects:

import {
  springDefault,
  springBouncy,
  springGentle,
  springSmooth,
  springSnappy,
  springStiff,
  springInstant,
} from '@gentleduck/motion'
import {
  springDefault,
  springBouncy,
  springGentle,
  springSmooth,
  springSnappy,
  springStiff,
  springInstant,
} from '@gentleduck/motion'
NameConfigUse case
springDefaultvisualDuration: 0.25, bounce: 0.2General-purpose spring
springSnappyvisualDuration: 0.2, bounce: 0.15Menus, dropdowns, popovers
springGentlevisualDuration: 0.35, bounce: 0.25Dialogs, sheets, drawers
springBouncystiffness: 500, damping: 28Menus and popovers with slight overshoot
springStiffstiffness: 400, damping: 30Alert dialogs, destructive confirmations
springSmoothstiffness: 350, damping: 30Layout indicators, sliding elements
springInstantstiffness: 1000, damping: 100Reduced-motion fallback

Presets

Pre-built animation configs ready to spread onto motion components.

Content presets

import {
  spinIn,
  slideUpBlur,
  fadeUp,
  scaleBlur,
  fadeBlur,
  collapseX,
  fadeBlurPopOut,
  blurMount,
  tapScale,
  contentTransition,
  contentTransitionFast,
} from '@gentleduck/motion'
import {
  spinIn,
  slideUpBlur,
  fadeUp,
  scaleBlur,
  fadeBlur,
  collapseX,
  fadeBlurPopOut,
  blurMount,
  tapScale,
  contentTransition,
  contentTransitionFast,
} from '@gentleduck/motion'
NameUse case
spinInIcon swaps, loading spinners, status indicators
slideUpBlurText reveals, labels, descriptions
fadeUpContent mount, tags, chips, toggle children
scaleBlurImages, media, aspect-ratio containers
fadeBlurButtons, containers, overlays
collapseXText/icon collapse inside buttons
fadeBlurPopOutCollapsible button children with popLayout
blurMountCSS-driven opacity (e.g. disabled states)
tapScaleUse as whileTap={tapScale} on buttons and toggles
contentTransition250ms expo-out for smooth reveals
contentTransitionFast150ms expo-out for button content swaps

Basic presets

import {
  fadeIn,
  fadeOut,
  popIn,
  rotateIn,
  scaleIn,
  slideUp,
  slideDown,
  slideFromLeft,
  slideFromRight,
} from '@gentleduck/motion'
import {
  fadeIn,
  fadeOut,
  popIn,
  rotateIn,
  scaleIn,
  slideUp,
  slideDown,
  slideFromLeft,
  slideFromRight,
} from '@gentleduck/motion'

All basic presets return { initial, animate, exit } objects. Spread them directly onto motion components:

<m.div {...fadeIn} transition={tweenFast}>
  Content
</m.div>
<m.div {...fadeIn} transition={tweenFast}>
  Content
</m.div>

heightAuto

Height expand/collapse preset for accordion and collapsible content. Stays mounted and toggles between open and closed states.

import { heightAuto } from '@gentleduck/motion'
 
<m.div animate={isOpen ? heightAuto.open : heightAuto.closed} transition={tweenExpand}>
  Collapsible content
</m.div>
import { heightAuto } from '@gentleduck/motion'
 
<m.div animate={isOpen ? heightAuto.open : heightAuto.closed} transition={tweenExpand}>
  Collapsible content
</m.div>

createDirectionalPreset

Factory for directional enter/exit animations with scale and blur.

import { createDirectionalPreset } from '@gentleduck/motion'
 
const fromBottom = createDirectionalPreset('bottom')
const fromTop = createDirectionalPreset('top', 12, 40, 12)
 
<m.div {...fromBottom} transition={springSnappy}>
  Drawer content
</m.div>
import { createDirectionalPreset } from '@gentleduck/motion'
 
const fromBottom = createDirectionalPreset('bottom')
const fromTop = createDirectionalPreset('top', 12, 40, 12)
 
<m.div {...fromBottom} transition={springSnappy}>
  Drawer content
</m.div>
ParameterTypeDefaultDescription
direction'top' | 'bottom' | 'left' | 'right'Enter direction
enterOffsetnumber8Offset in px for enter animation
exitOffsetnumber30Offset in px for exit animation
blurnumber8Blur amount in px

Variants

CVA-based variant utilities for applying animation classes.

AnimVariants

CVA variant for applying transition properties to animated elements.

import { AnimVariants } from '@gentleduck/motion'
 
<div className={AnimVariants({ alive: 'default', pseudo: 'animate' })}>
  Animated content
</div>
import { AnimVariants } from '@gentleduck/motion'
 
<div className={AnimVariants({ alive: 'default', pseudo: 'animate' })}>
  Animated content
</div>
VariantValuesDescription
alive'default'Applies transition-all, duration, and ease classes
pseudo'animate' | 'default'Applies GPU-accelerated pseudo-element transitions

checkersStylePattern

CVA variant for checkbox, radio, and switch patterns.

import { checkersStylePattern } from '@gentleduck/motion'
 
<input
  type='checkbox'
  className={checkersStylePattern({ type: 'checkbox', indicatorState: 'both' })}
/>
import { checkersStylePattern } from '@gentleduck/motion'
 
<input
  type='checkbox'
  className={checkersStylePattern({ type: 'checkbox', indicatorState: 'both' })}
/>
VariantValuesDescription
type'checkbox' | 'radio' | 'switch'Control type — drives shape, padding, and animation direction
indicatorState'default' | 'both' | 'indicatorReady' | 'checkedIndicatorReady'Mask image strategy for the indicator SVG

Reduced-motion API

useDuckReducedMotion

Returns true if the user prefers reduced motion. Uses useSyncExternalStore for efficient media query subscription.

import { useDuckReducedMotion } from '@gentleduck/motion'
 
function MyComponent() {
  const reduced = useDuckReducedMotion()
  return <m.div transition={reduced ? { duration: 0 } : tweenNormal} />
}
import { useDuckReducedMotion } from '@gentleduck/motion'
 
function MyComponent() {
  const reduced = useDuckReducedMotion()
  return <m.div transition={reduced ? { duration: 0 } : tweenNormal} />
}

motionTransition

Returns the normal transition, or { duration: 0 } when reduced is true.

import { motionTransition, useDuckReducedMotion } from '@gentleduck/motion'
 
const reduced = useDuckReducedMotion()
const transition = motionTransition(reduced, tweenNormal)
import { motionTransition, useDuckReducedMotion } from '@gentleduck/motion'
 
const reduced = useDuckReducedMotion()
const transition = motionTransition(reduced, tweenNormal)

onDuckReducedMotionChange

Subscribe to changes in the reduced-motion preference. Returns an unsubscribe function.

import { onDuckReducedMotionChange } from '@gentleduck/motion'
 
const unsubscribe = onDuckReducedMotionChange(() => {
  console.log('reduced motion preference changed')
})
import { onDuckReducedMotionChange } from '@gentleduck/motion'
 
const unsubscribe = onDuckReducedMotionChange(() => {
  console.log('reduced motion preference changed')
})

getDuckReducedMotionServerSnapshot

SSR-safe snapshot — always returns false on the server.

import { getDuckReducedMotionServerSnapshot } from '@gentleduck/motion'
 
const reduced = getDuckReducedMotionServerSnapshot() // false
import { getDuckReducedMotionServerSnapshot } from '@gentleduck/motion'
 
const reduced = getDuckReducedMotionServerSnapshot() // false

IReducedMotionFallback

Type for the reduced-motion fallback object { duration: 0 }.

import type { IReducedMotionFallback } from '@gentleduck/motion'
import type { IReducedMotionFallback } from '@gentleduck/motion'

Backward-compat tokens

These tokens are still exported but prefer the newer names.

import { duckDuration, duckEasing, duckMotionCssVar } from '@gentleduck/motion'
import { duckDuration, duckEasing, duckMotionCssVar } from '@gentleduck/motion'
ExportMaps toDescription
duckDurationduckMotionDurationMsDuration values in milliseconds
duckEasingduckMotionEasingCssEasing as CSS cubic-bezier() strings
duckMotionCssVarCSS variable references with fallbacks

CSS entrypoint

import '@gentleduck/motion/css'
import '@gentleduck/motion/css'

Provides motion tokens and prefers-reduced-motion defaults:

  • --gentleduck-motion-ease — cubic-bezier easing curve
  • --gentleduck-motion-spring — spring-like easing via linear() keyframe stops
  • --gentleduck-motion-dur — default duration (150ms)