opción

motion-foundations

affaan-m/ECC affaan-m/ECC

Tokens de movimiento, ajustes predefinidos de Spring, reglas de rendimiento, adaptación a dispositivos, aplicación de la accesibilidad y seguridad SSR para React/Next.js mediante motion/react. Capa base: el resto de habilidades relacionadas con el movimiento dependen de ella.

...Expandir todo
51
Tiempo actualizado 29 de julio de 2026

Fundamentos del sistema de movimiento

La capa base del sistema de movimiento. Define todos los valores, restricciones y reglas que heredan las habilidades posteriores (motion-patterns, motion-advanced) heredan. Carga esta habilidad antes de comenzar cualquier trabajo de animación.

Cuándo activarla

  • Al iniciar cualquier componente animado desde cero
  • Al configurar tokens, ajustes preestablecidos de muelles o valores de aceleración/deceleración
  • Al implementar prefers-reduced-motion compatibilidad
  • Depurar discrepancias de hidratación en los estados iniciales de la animación
  • Evaluar si una animación debería existir

Resultados

Esta habilidad genera:

  • Un objeto compartido motionTokens (duración, aceleración, distancia, escala)
  • Un springs mapa de preajustes compartido (5 configuraciones con nombre)
  • Una shouldAnimate() puerta utilizada por todos los componentes
  • Valores predeterminados de animación que cumplen con las normas de accesibilidad a través de useReducedMotion
  • estados iniciales compatibles con SSR y sin advertencias de hidratación

Principios

El movimiento debe cumplir al menos uno de los siguientes requisitos o, de lo contrario, debe eliminarse:

  • Dirigir la atención
  • Comunicar el estado
  • Preservar la continuidad espacial

La capacidad de respuesta siempre prima sobre la fluidez. Una animación a 60 fps que provoque un retraso en la respuesta es peor que la ausencia de animación.

Reglas

Estas normas son innegociables. Se aplican a todos los componentes del sistema.

  1. Utiliza únicamente «motion/react». Nunca importes desde framer-motion. Nunca mezcles ambos en el mismo árbol.
  2. initial debe coincidir con la salida del servidor. Si el servidor renderiza opacity: 1, la initial prop también debe ser opacity: 1. Sin excepciones.
  3. La reducción de movimiento tiene prioridad sobre todo lo demás. Cuando useReducedMotion() devuelve true o prefersReduced está true, todas las transformaciones quedan desactivadas. Los fundidos que solo afectan a la opacidad en ≤ 0,2 s son la única alternativa permitida.
  4. Nunca se deben animar las propiedades de diseño. width, height, top, left, margin, padding están prohibidas en animate. Utiliza transform y opacity exclusivamente.
  5. Todos los valores de los tokens proceden de motionTokens. Quedan prohibidas las duraciones y aceleraciones codificadas de forma fija en los archivos de los componentes.
  6. Todas las configuraciones de Spring proceden del mapa springs. Los stiffness/damping están prohibidos.
  7. "use client" es obligatorio en todos los archivos que importen desde motion/react.
  8. Nunca leas window ni navigator a nivel de módulo. Utiliza siempre la protección con typeof window !== "undefined".

Orientación para la toma de decisiones

Elección de una duración

Elegir un resorte

Cuándo desactivar la animación por completo

Desactivar (hacer que shouldAnimate() volver false) cuando:

  • prefersReduced sea true
  • isLowEnd y true y la animación no sea esencial
  • El elemento está fuera de la pantalla y nunca entrará en el área de visualización
  • La animación es puramente decorativa y no tiene ninguna finalidad relacionada con la experiencia de usuario

Conceptos básicos

Sistema de tokens

// lib/motion-tokens.ts
export const motionTokens = {
  duration: {
    instant: 0.08,
    fast:    0.18,
    normal:  0.35,
    slow:    0.6,
    crawl:   1.0,
  },
  easing: {
    smooth: [0.22, 1, 0.36, 1],
    sharp:  [0.4, 0, 0.2, 1],
    bounce: [0.34, 1.56, 0.64, 1],
    linear: [0, 0, 1, 1],
  },
  distance: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 48,
  },
  scale: {
    subtle: 0.98,
    press:  0.95,
    pop:    1.04,
  },
}

export const springs = {
  snappy:  { type: "spring", stiffness: 300, damping: 30 },
  gentle:  { type: "spring", stiffness: 120, damping: 14 },
  bouncy:  { type: "spring", stiffness: 400, damping: 10 },
  instant: { type: "spring", stiffness: 600, damping: 35 },
  release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
}

Indicadores de tiempo de ejecución

// lib/motion-config.ts
export const motionConfig = {
  isLowEnd() {
    return (
      typeof navigator !== "undefined" &&
      navigator.hardwareConcurrency <= 4
    )
  },

  prefersReduced() {
    return (
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    )
  },

  shouldAnimate({ essential = false } = {}) {
    if (this.prefersReduced()) return false
    if (!essential && this.isLowEnd()) return false
    return true
  },

  duration() {
    return this.isLowEnd() || this.prefersReduced()
      ? motionTokens.duration.instant
      : motionTokens.duration.normal
  },
}

Accesibilidad

Orden de prioridad (de mayor a menor):

  1. prefers-reduced-motion: reduce — desactiva todas las transformaciones y limita las transiciones de opacidad a ≤ 0,2 s
  2. Detección de dispositivos de gama baja — reduce la duración y elimina las animaciones no esenciales
  3. Preferencias de diseño — todo lo demás

El movimiento debe degradarse de forma fluida. Nunca debe desaparecer bruscamente de manera que provoque cambios en el diseño o confunda la orientación.

// hooks/use-reduced-motion.tsx
"use client"
import { useReducedMotion } from "motion/react"

export function useSafeMotion(fullY: number = 16) {
  const reduce = useReducedMotion()
  return {
    initial: { opacity: 0, y: reduce ? 0 : fullY },
    animate: { opacity: 1, y: 0 },
    exit:    { opacity: 0, y: reduce ? 0 : -fullY },
  }
}
/* globals.css */
@media (prefers-reduced-motion: reduce) {
  .motion-safe-transition  { transition: opacity 0.15s; }
  .motion-reduce-transform { transform: none !important; }
}

<div class="motion-safe:animate-fade motion-reduce:opacity-100">div>

Seguridad de SSR / hidratación

Regla: «initial» debe coincidir siempre con lo que renderiza el servidor.

// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />

// CORRECT — use AnimatePresence or defer to client mount
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])

div
  initial={{ opacity: mounted ? 0 : 1 }}
  animate={{ opacity: 1 }}
/>

Ejemplos de código

De extremo a extremo: tokens + springs + accesibilidad + protección de SSR

// components/fade-in-card.tsx
"use client"

import { useState, useEffect } from "react"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"
import { motionConfig } from "@/lib/motion-config"

interface FadeInCardProps {
  children: React.ReactNode
  delay?: number
}

export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
  // SSR guard — initial must match server output (opacity: 1)
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])

  // Accessibility — disables transform when reduced motion is preferred
  const safeMotion = useSafeMotion(motionTokens.distance.md)

  // Device gate — skip animation on low-end hardware
  if (!motionConfig.shouldAnimate() || !mounted) {
    return <div>{children}div>
  }

  return (
    <motion.div
      initial={safeMotion.initial}
      animate={safeMotion.animate}
      exit={safeMotion.exit}
      transition={{
        ...springs.gentle,
        delay,
      }}
      whileHover={{ scale: motionTokens.scale.pop }}
      whileTap={{ scale: motionTokens.scale.press }}
    >
      {children}
    motion.div>
  )
}

Restricciones / Objetivos excluidos

Esta habilidad no abarca:

  • Patrones de componentes de la interfaz de usuario (botones, modales, stagger) → véase motion-patterns
  • Arrastrar, gestos, SVG, animaciones de texto, hooks personalizados → véase motion-advanced
  • Animaciones solo con CSS o clases de Tailwind animate-* sin motion/react
  • bibliotecas de animación de terceros (GSAP, anime.js, etc.)
  • Decisiones de diseño de movimiento (cuándo animar, qué destacar): eso es una cuestión de diseño, no una limitación del código

Antipatrones

Habilidades relacionadas

  • motion-patterns — utiliza los tokens y los resortes definidos aquí para crear patrones de botones, ventanas modales, escalonamiento, transiciones de página y desplazamiento. No redefine ningún valor.
  • motion-advanced — Utiliza los tokens y los «springs» definidos aquí para patrones de arrastre, SVG, texto y gestos. Añade useAnimate secuencias y ganchos personalizados sobre esta base.
Ver en GitHub

Motion Foundations

The base layer of the motion system. Defines every value, constraint, and rule that downstream skills (motion-patterns, motion-advanced) inherit. Load this skill before any animation work begins.

When to Activate

  • Starting any animated component from scratch
  • Setting up tokens, spring presets, or easing values
  • Implementing prefers-reduced-motion support
  • Debugging hydration mismatches from animation initial states
  • Evaluating whether an animation should exist at all

Outputs

This skill produces:

  • A shared motionTokens object (duration, easing, distance, scale)
  • A shared springs preset map (5 named configs)
  • A shouldAnimate() gate used by all components
  • Accessibility-compliant animation defaults via useReducedMotion
  • SSR-safe initial states with zero hydration warnings

Principles

Motion must do at least one of the following or it must be removed:

  • Guide attention
  • Communicate state
  • Preserve spatial continuity

Responsiveness always outranks smoothness. A 60 fps animation that causes input delay is worse than no animation.

Rules

These are non-negotiable. They apply to every component in the system.

  1. Use motion/react only. Never import from framer-motion. Never mix the two in the same tree.
  2. initial must match server output. If the server renders opacity: 1, the initial prop must also be opacity: 1. No exceptions.
  3. Reduced motion overrides everything. When useReducedMotion() returns true or prefersReduced is true, all transforms are disabled. Opacity-only fades at ≤ 0.2s are the only permitted fallback.
  4. Never animate layout properties. width, height, top, left, margin, padding are banned from animate. Use transform and opacity only.
  5. All token values come from motionTokens. Hardcoded durations and easings in component files are forbidden.
  6. All spring configs come from the springs map. Inline stiffness/damping values are forbidden.
  7. "use client" is required on every file that imports from motion/react.
  8. Never read window or navigator at module level. Always guard with typeof window !== "undefined".

Decision Guidance

Choosing a duration

Choosing a spring

When to disable animation entirely

Disable (make shouldAnimate() return false) when:

  • prefersReduced is true
  • isLowEnd is true and the animation is non-essential
  • The element is off-screen and will never enter the viewport
  • The animation is purely decorative with no UX purpose

Core Concepts

Token system

// lib/motion-tokens.ts
export const motionTokens = {
  duration: {
    instant: 0.08,
    fast:    0.18,
    normal:  0.35,
    slow:    0.6,
    crawl:   1.0,
  },
  easing: {
    smooth: [0.22, 1, 0.36, 1],
    sharp:  [0.4, 0, 0.2, 1],
    bounce: [0.34, 1.56, 0.64, 1],
    linear: [0, 0, 1, 1],
  },
  distance: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 48,
  },
  scale: {
    subtle: 0.98,
    press:  0.95,
    pop:    1.04,
  },
}

export const springs = {
  snappy:  { type: "spring", stiffness: 300, damping: 30 },
  gentle:  { type: "spring", stiffness: 120, damping: 14 },
  bouncy:  { type: "spring", stiffness: 400, damping: 10 },
  instant: { type: "spring", stiffness: 600, damping: 35 },
  release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
}

Runtime flags

// lib/motion-config.ts
export const motionConfig = {
  isLowEnd() {
    return (
      typeof navigator !== "undefined" &&
      navigator.hardwareConcurrency <= 4
    )
  },

  prefersReduced() {
    return (
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    )
  },

  shouldAnimate({ essential = false } = {}) {
    if (this.prefersReduced()) return false
    if (!essential && this.isLowEnd()) return false
    return true
  },

  duration() {
    return this.isLowEnd() || this.prefersReduced()
      ? motionTokens.duration.instant
      : motionTokens.duration.normal
  },
}

Accessibility

Priority order (highest to lowest):

  1. prefers-reduced-motion: reduce — disables all transforms, limits opacity transitions to ≤ 0.2s
  2. Low-end device detection — reduces duration, removes non-essential animations
  3. Design preference — everything else

Motion must degrade gracefully. It must never disappear abruptly in a way that causes layout shift or confuses orientation.

// hooks/use-reduced-motion.tsx
"use client"
import { useReducedMotion } from "motion/react"

export function useSafeMotion(fullY: number = 16) {
  const reduce = useReducedMotion()
  return {
    initial: { opacity: 0, y: reduce ? 0 : fullY },
    animate: { opacity: 1, y: 0 },
    exit:    { opacity: 0, y: reduce ? 0 : -fullY },
  }
}
/* globals.css */
@media (prefers-reduced-motion: reduce) {
  .motion-safe-transition  { transition: opacity 0.15s; }
  .motion-reduce-transform { transform: none !important; }
}
<!-- Tailwind -->
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>

SSR / hydration safety

Rule: initial must always match what the server renders.

// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />

// CORRECT — use AnimatePresence or defer to client mount
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])

<motion.div
  initial={{ opacity: mounted ? 0 : 1 }}
  animate={{ opacity: 1 }}
/>

Code Examples

End-to-end: tokens + springs + accessibility + SSR guard

// components/fade-in-card.tsx
"use client"

import { useState, useEffect } from "react"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"
import { motionConfig } from "@/lib/motion-config"

interface FadeInCardProps {
  children: React.ReactNode
  delay?: number
}

export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
  // SSR guard — initial must match server output (opacity: 1)
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])

  // Accessibility — disables transform when reduced motion is preferred
  const safeMotion = useSafeMotion(motionTokens.distance.md)

  // Device gate — skip animation on low-end hardware
  if (!motionConfig.shouldAnimate() || !mounted) {
    return <div>{children}</div>
  }

  return (
    <motion.div
      initial={safeMotion.initial}
      animate={safeMotion.animate}
      exit={safeMotion.exit}
      transition={{
        ...springs.gentle,
        delay,
      }}
      whileHover={{ scale: motionTokens.scale.pop }}
      whileTap={{ scale: motionTokens.scale.press }}
    >
      {children}
    </motion.div>
  )
}

Constraints / Non-Goals

This skill does not cover:

  • UI component patterns (button, modal, stagger) → see motion-patterns
  • Drag, gestures, SVG, text animations, custom hooks → see motion-advanced
  • CSS-only animations or Tailwind animate-* classes without motion/react
  • Third-party animation libraries (GSAP, anime.js, etc.)
  • Motion design decisions (when to animate, what to emphasize) — that is a design concern, not a code constraint

Anti-Patterns

Related Skills

  • motion-patterns — consumes tokens and springs defined here to build button, modal, stagger, page transition, and scroll patterns. Does not redefine any values.
  • motion-advanced — consumes tokens and springs defined here for drag, SVG, text, and gesture patterns. Adds useAnimate sequences and custom hooks on top of this foundation.

Todos los archivos

1 archivos
SKILL.md 9.4k
Ver

Instalar motion-foundations

Descarga y descomprime los archivos de las habilidades en tu directorio .claude/skills/.

Descargar ZIP

Clona el repositorio y copia los archivos de la habilidad a tu proyecto.

git clone https://github.com/affaan-m/ECC/tree/main/skills/motion-foundations # Copy the skill folder to .claude/skills/ or .codex/skills/

Copiar Copiar
Configuración rápida: Copia la carpeta de la habilidad en .claude/skills/. Claude la detectará automáticamente y la utilizará.
Repositorio affaan-m/ECC

Habilidades relacionadas

web-search
Tiempo actualizado 29 de junio de 2026
computer-use
Tiempo actualizado 29 de julio de 2026
webapp-testing
Tiempo actualizado 29 de junio de 2026
lark-base
Tiempo actualizado 5 de julio de 2026
OR