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'
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>
  )
}
PropTypeDefaultDescription
childrenReact.ReactNode—Content to render inside the provider
transitionTransition{ duration: 0.15, ease: [0.4, 0, 0.2, 1] }Global default transition
enterTransitionTransition—Override transition for enter animations only
exitTransitionTransition—Override 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()
  // ...
}

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

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


Stagger Utilities

Helpers for staggered enter animations.

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
ParameterTypeDefaultDescription
indexnumber—Item index
staggerMsnumber—Per-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>
))

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>

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
duckMotionDurationMs.exit     // 180 (fallback used when a transition has no measurable duration)

Easing tokens

import { duckMotionEasing, duckMotionEasingCss } from '@gentleduck/motion'

// Cubic-bezier arrays (for motion library)
duckMotionEasing.standard  // [0.4, 0, 0.2, 1]
duckMotionEasing.exit      // [0.4, 0, 1, 1] (accelerating ease-in, for exit animations)
duckMotionEasing.expo      // [0.16, 1, 0.3, 1] (expo-out, for expand/collapse and reveals)
duckMotionEasing.easeOut   // [0, 0, 0.2, 1] (for micro-interactions and tooltips)
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)'

CSS variable reference

import { duckMotionCssVar } from '@gentleduck/motion/transitions'

duckMotionCssVar.duration  // 'var(--gentleduck-motion-dur, 150ms)'
duckMotionCssVar.easing    // 'var(--gentleduck-motion-ease, cubic-bezier(0.4, 0, 0.2, 1))'

Tween presets

Ready-to-use tween Transition objects, from the /transitions subpath:

import {
  tweenNormal,
  tweenSlow,
  tweenExit,
  tweenExpand,
  tweenShake,
} from '@gentleduck/motion/transitions'
NameDurationEaseUse case
tweenNormal200msstandardOverlays, content reveals
tweenSlow300msstandardLarge layout changes, page transitions
tweenExit150msexitClosing menus and dialogs
tweenExpand250msexpo-outAccordion, collapsible, height reveals
tweenShake400ms—Error shake feedback

There is no tweenFast, tweenMicro, or tweenInstant preset. For sub-150ms feedback, build a one-off Transition from the duckMotionDuration tokens — see tapScale below for the pattern the package itself uses (a 15ms ease-out tween for tap press/release).

Spring presets

Ready-to-use spring Transition objects, from the /transitions subpath:

import {
  springDefault,
  springBouncy,
  springGentle,
  springSmooth,
  springSnappy,
  springStiff,
  springInstant,
} from '@gentleduck/motion/transitions'
NameConfigUse case
springDefaultvisualDuration: 0.25, bounce: 0.2General-purpose spring
springSnappyvisualDuration: 0.2, bounce: 0.15Small staggered reveals, such as breadcrumb items
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. All live under the /presets subpath.

Content presets

import {
  spinIn,
  tapScale,
  checkerBounce,
  contentTransition,
  contentTransitionFast,
} from '@gentleduck/motion/presets'
NameUse case
spinInIcon swaps, loading spinners, status indicators
tapScaleUse as whileTap={tapScale} on buttons and toggles (15ms ease-out)
checkerBounceBounce keyframes for toggle controls: checkboxes, radio buttons, switches
contentTransition250ms expo-out for smooth reveals
contentTransitionFast150ms expo-out for button content swaps

Basic presets

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

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

<m.div {...fadeIn} transition={tweenNormal}>
  Content
</m.div>
NameUse case
fadeInPure opacity fade, no scale or blur
popInBadges, chips, notification dots, count indicators
rotateInModals, drawers, dialogs, contextual panels
scaleInOverlays and content reveals
slideUpText reveals, labels, list items anchored to the top
slideDownText reveals, labels, list items anchored to the bottom
slideFromLeftSidebar reveals and breadcrumb chevrons
slideFromRightSidebar reveals and trailing actions

There is no fadeOut preset; fadeIn's exit state already fades to opacity: 0.

heightAuto

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

import { heightAuto } from '@gentleduck/motion/presets'

<m.div animate={isOpen ? heightAuto.open : heightAuto.closed} transition={tweenExpand}>
  Collapsible content
</m.div>

createDirectionalPreset and friends

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

import { createDirectionalPreset } from '@gentleduck/motion/presets'

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
initialScalenumber0.95Enter scale
exitScalenumber0.9Exit scale

Three more factories build on it for specific shapes:

import { createSlidePreset, createSlideEdge, createTooltipPreset } from '@gentleduck/motion/presets'
FactorySignatureUse case
createSlidePreset(direction, enterOffset = 8, exitOffset = 30)Pure slide, no scale; what slideUp/slideDown/slideFromLeft/slideFromRight are built from
createSlideEdge(side)Edge-anchored panels (sheets, drawers) that slide in from 100% off-screen
createTooltipPreset(side, offset = 4)Tooltips and hover cards that shift toward the trigger on enter

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>
VariantValuesDescription
alive'default'Transitions opacity and transforms (not colours) over 200ms with --gentleduck-motion-ease
pseudo'animate' | 'default'Applies the same opacity/transform transition to :before/:after, inheriting the host's duration and easing

checkersStylePattern

CVA variant for checkbox, radio, and switch patterns.

import { checkersStylePattern } from '@gentleduck/motion'

<input
  type='checkbox'
  className={checkersStylePattern({ type: 'checkbox', indicatorState: 'both' })}
/>
VariantValuesDescription
type'checkbox' | 'radio' | 'switch'Control type, which 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. Built on useSyncExternalStore, so it is SSR-safe (returns false on the server) and re-renders on change without a manual subscription.

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)

IReducedMotionFallback

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

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

CSS entrypoint

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)