вариант

motion-foundations

affaan-m/ECC affaan-m/ECC

Токены движения, предустановки пружин, правила исполнения, адаптация к устройствам, обеспечение доступности и безопасность SSR для React / Next.js с использованием motion/react. Базовый уровень — от него зависят все остальные навыки работы с движением.

...Расширить все
51
Обновлено время 29 июля 2026 г.

Основы движения

Базовый уровень системы анимации. Определяет все значения, ограничения и правила, которые наследуют последующие навыки (motion-patterns, motion-advanced) унаследуют. Загрузите этот навык до начала любой работы с анимацией.

Когда активировать

  • При запуске любого анимированного компонента с нуля
  • Настройка токенов, пресетов пружин или значений плавности
  • Реализация prefers-reduced-motion поддержки
  • Отладка несоответствий гидратации исходных состояний анимации
  • Оценка необходимости существования анимации

Результаты

Этот навык генерирует:

  • Общий motionTokens объект (продолжительность, плавность, расстояние, масштаб)
  • Общую springs карту пресетов (5 именованных конфигураций)
  • A shouldAnimate() шлюз, используемый всеми компонентами
  • Анимация по умолчанию, соответствующая требованиям доступности, посредством useReducedMotion
  • безопасных для SSR начальных состояний без предупреждений о гидратации

Принципы

Анимация должна выполнять хотя бы одно из следующих действий, иначе её необходимо удалить:

  • Привлекать внимание
  • Сообщать о состоянии
  • Сохранять пространственную непрерывность

Отзывчивость всегда важнее плавности. Анимация со скоростью 60 кадров в секунду, вызывающая задержку ввода, хуже, чем отсутствие анимации.

Правила

Эти правила не подлежат обсуждению. Они применимы ко всем компонентам системы.

  1. Используйте только motion/react. Никогда не импортируйте из framer-motion. Никогда не смешивайте эти два метода в одном дереве.
  2. initial должны совпадать с выводом сервера. Если сервер отображает opacity: 1, то initial prop также должен быть opacity: 1. Без исключений.
  3. Упрощённое движение имеет приоритет над всем остальным. Когда useReducedMotion() возвращает true или prefersReduced имеет значение true, все преобразования отключаются. Единственным допустимым запасным вариантом являются затухания, основанные исключительно на прозрачности, длительностью ≤ 0,2 с.
  4. Никогда не анимируйте свойства верстки. width, height, top, left, margin, padding запрещены в animate. Используйте transform и opacity только.
  5. Все значения токенов берутся из файла `motionTokens`. Жестко заданные значения продолжительности и ускорения в файлах компонентов запрещены.
  6. Все настройки пружин берутся из карты springs. Использование stiffness/damping значения запрещены.
  7. "use client" требуется в каждом файле, который импортирует из motion/react.
  8. Никогда не читайте window или navigator на уровне модуля. Всегда используйте защиту с помощью typeof window !== "undefined".

Рекомендации по принятию решений

Выбор продолжительности

Выбор пружины

Когда следует полностью отключить анимацию

Отключить (сделать shouldAnimate() возврат false) в следующих случаях:

  • prefersReduced есть true
  • isLowEnd и true и анимация не является существенной
  • Элемент находится за пределами экрана и никогда не попадет в область просмотра
  • анимация носит чисто декоративный характер и не преследует целей пользовательского опыта

Основные концепции

Система токенов

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

Флаги на этапе выполнения

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

Доступность

Порядок приоритетов (от наивысшего к наинизшему):

  1. prefers-reduced-motion: reduce — отключает все преобразования, ограничивает переходы непрозрачности до ≤ 0,2 с
  2. Обнаружение устройств низкого уровня — сокращает продолжительность, удаляет несущественные анимации
  3. Предпочтения дизайна — всё остальное

Анимация должна плавно ухудшаться. Она ни в коем случае не должна исчезать резко, так, чтобы это приводило к сдвигу макета или сбивало с ориентации.

// 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>

Безопасность SSR / гидратации

Правило: «initial» всегда должны соответствовать тому, что отображает сервер.

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

Примеры кода

Сквозной подход: токены + пружины + доступность + защита 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>
  )
}

Ограничения / Нецели

Данный навык не охватывает:

  • Шаблоны компонентов пользовательского интерфейса (кнопки, модальные окна, расположение в шахматном порядке) → см. motion-patterns
  • Перетаскивание, жесты, SVG, анимация текста, пользовательские хуки → см. motion-advanced
  • Анимации, реализованные исключительно с помощью CSS, или классы Tailwind animate-* без motion/react
  • сторонних библиотек анимации (GSAP, anime.js и т. д.)
  • Решения по моушн-дизайну (когда применять анимацию, что выделить) — это вопрос дизайна, а не ограничение кода

Антипаттерны

Связанные навыки

  • motion-patterns — использует токены и пружины, определённые здесь, для создания шаблонов кнопок, модальных окон, ступенчатого отображения, переходов между страницами и прокрутки. Не переопределяет никаких значений.
  • motion-advanced — использует токены и пружины, определённые здесь, для шаблонов перетаскивания, SVG, текста и жестов. Добавляет useAnimate последовательности и пользовательские хуки поверх этой основы
Посмотреть на 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.

Все файлы

1 файлов

Установить motion-foundations

Скачайте файлы навыков и распакуйте их в каталог .claude/skills/.

Скачать ZIP

Клонируйте репозиторий и скопируйте файлы навыка в свой проект.

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

Копировать Копировать
Быстрая настройка: Скопируйте папку со скиллом в каталог .claude/skills/ — Claude автоматически обнаружит и начнет использовать этот скилл
Репозиторий affaan-m/ECC

Похожие навыки

web-search
Обновлено время 29 июня 2026 г.
computer-use
Обновлено время 29 июля 2026 г.
webapp-testing
Обновлено время 29 июня 2026 г.
lark-base
Обновлено время 5 июля 2026 г.
OR