Ziteox Forge

Animation Guidelines

CSS vs motion animation strategy.

Animation Guidelines

How @ziteox/ui handles motion and when to use each approach.

Library standard

LibraryStatus
motion (motion/react)Standard — use for complex animation
framer-motionDo not add — migrate to motion if importing legacy code

motion is a peer dependency. Apps install it once.

Decision tree

Does the animation need spring physics, layout animation,
AnimatePresence, or multi-property SVG morph?
├── Yes → use motion
└── No → can CSS transitions handle it?
    ├── Yes → use CSS (preferred for simplicity)
    └── No → use motion

CSS transitions

Use for:

  • Simple state morphs (hamburger → X)
  • Opacity / color hovers
  • Single-element transforms

Current example: HamburgerButton

Requirements:

  • Drive state via attributes (aria-expanded) or classes, not inline style animation
  • Use usePrefersReducedMotion to shorten or disable transitions
  • Use useSkipInitialAnimation to prevent hydration flash
const skipInitialAnimation = useSkipInitialAnimation();
const prefersReducedMotion = usePrefersReducedMotion();

motion

Use for:

  • Spring-based SVG morphs (AnimatedThemeToggler)
  • Enter/exit with AnimatePresence (future MobileMenu)
  • Staggered children
  • layoutId shared layout animations

Presets: define springs in animations/springs.ts:

export const iconSpring = {
  type: "spring" as const,
  stiffness: 380,
  damping: 30,
};

Do not duplicate spring constants inline in components.

Future sheet/menu springs (not yet in package): add to animations/ when MobileMenu is implemented — do not pre-build unused presets.

Shared hooks (internal)

HookPurpose
useSkipInitialAnimationReturns true on first render; disables initial transition
usePrefersReducedMotionReads (prefers-reduced-motion: reduce)
useMountedfalse until after mount; hydration-safe rendering

Located in packages/ui/src/shared/hooks/. Not exported publicly.

Skip initial animation pattern

const skipInitialAnimation = useSkipInitialAnimation();
const transition = skipInitialAnimation ? noTransition : iconSpring;

For CSS components, apply modifier class when skipInitialAnimation is true.

Reduced motion pattern

const prefersReducedMotion = usePrefersReducedMotion();
// motion: use noTransition or duration 0
// CSS: shorter duration class or transition: none

When both apply, prefer no animation.

Audio feedback

Procedural Web Audio (see theme/play-tick.ts) is optional per component.

  • Colocate in domain folder until second consumer exists
  • Fail silently if API unavailable
  • Gate with prop (sound={false})

What to avoid

  • Adding animation libraries beyond motion
  • Animating layout properties without performance consideration
  • Hardcoded duration: 300 in every file — extract or use presets
  • Relying on parent group class for hover — scope hover on component root
  • Module-level animation side effects (sideEffects: false in package.json)

Component reference

ComponentAnimation typeLibrary
AnimatedThemeTogglerSVG spring morph + button scalemotion
HamburgerButtonBar transform morphCSS
BrandCreditHeart color on hoverCSS
ThemeToggleDelegates to AnimatedThemeTogglermotion (indirect)