Skip to content

Repository files navigation

@soichiro_nitta/motion

https://www.npmjs.com/package/@soichiro_nitta/motion

軽量な DOM トランジションユーティリティ。要素の transform/CSS を手早くアニメーションさせるためのミニマル API を提供します。

インストール

pnpm add @soichiro_nitta/motion

クイックスタート(Next.js 推奨構成)

  1. ID を共通モジュール(例: app/id.ts)で定義
import { createId } from '@soichiro_nitta/motion'

export const ID = createId(['BOX', 'TITLE'])

Next.js App Router ではこの ID モジュールを RSC/Client どちらからでも import できるため、ID を一箇所で管理できます。

  1. クライアント専用モジュールを用意(例: app/motion.ts
'use client'
import { createMotion } from '@soichiro_nitta/motion'
import { ID } from './id'

// `createMotion(ID)` により、第一引数の候補は `keyof typeof ID` になる
export const { motion } = createMotion(ID)
  1. コンポーネントで利用
'use client'
import { useEffectAsync } from '@soichiro_nitta/motion'
import { ID } from '@/app/id'
import { motion } from '@/app/motion'

const Page = () => {
  useEffectAsync(async () => {
    await motion.delay(0.1)
    await motion.to('BOX', 0.5, 'out', { opacity: '1' })
    await motion.to('TITLE', 0.5, 'out', { opacity: '1', translateY: '0px' })
  }, [])

  return (
    <div>
      <div id={ID.BOX.N} style={{ opacity: 0 }}>
        Hello
      </div>
      <h1 id={ID.TITLE.N}>Title</h1>
    </div>
  )
}

export default Page
  • useEffectAsyncasync/await をそのまま書けるようにするラッパーで、内部で void を付けたり Promise を捨てたりする必要がありません。
  • cleanup が必要な場合は、useEffectAsync(async (c) => { ... })c(() => ...) で登録してください。
  • motion.run は即時実行のためのヘルパーで、(async () => { ... })() の代わりに motion.run(async () => { ... }) と書けます(返り値はそのまま Promise)。
  • Next.js 16 でのフル実装例(RSC + Client 分離)は検証リポジトリ motion-rsc-test を参照してください。

ID の使い方

createIdcreateMotion が返す ID はキーごとに N(id 文字列)と E()(DOM 参照)を持ち、motion.* で利用する第1引数のキーとも一致します。

  • ID.BOX.N: RSC でも安全な id 文字列。<div id={ID.BOX.N} /> のように使えます。
  • ID.BOX.E(): ブラウザで HTMLElement を返す関数。'use client' なモジュール内でのみ利用できます。
  • ID.BOX.E() は初回アクセス時に document.getElementById で取得した要素をキャッシュし、2 回目以降は同じ参照を返すため、繰り返し操作でも DOM 探索コストを抑えられます。
  • motion.to('BOX', …)'BOX' は上記キーのエイリアスで、DOM 探索をライブラリが肩代わりします。ID.BOX.E() で取得した要素を直接渡すことも可能です。
const elementBox = ID.BOX.E()
await motion.to(elementBox, 0.2, 'out', { opacity: '1' })
elementBox.style.borderRadius = '12px'

素の DOM での利用

import { createMotion } from '@soichiro_nitta/motion'

const { ID, motion } = createMotion(['BOX'])
await motion.to('BOX', 0.3, 'inout', { translateX: '20px', opacity: '0.8' })

API

  • createId(names: string[])

    • サーバー(RSC)でも安全に利用できる ID 辞書を返します。
    • 返り値: { ID }ID.NAME.N のみ保持)
  • createMotion(names: string[])

    • 指定した id 名、または createId が返す ID 辞書を受け取り、DOM アクセス付き ID と操作関数群 motion を返します。
    • 返り値: { ID, motion }
  • ID[name]

    • name に紐づく要素参照のヘルパ。ID.BOX.Nid 文字列、ID.BOX.E()HTMLElement を返します。
    • createMotion(ID) を利用すると motion.* の第一引数が keyof typeof ID(例: 'BOX' | 'TITLE') で補完されます。
  • motion.set(target, values)

    • 直ちにスタイルを適用します(トランジションなし)。transitionDuration 未指定時は 0s を自動セットします。
  • motion.run(task: () => Promise<unknown>)

    • 即時実行用のユーティリティ。(async () => { ... })() の代替として使えます。
    • await motion.run(async () => { await motion.to(...); }) のように await すれば、内部の最後の motion.to 完了まで待ってから次の行へ進みます。

即時関数パターンの置き換え例

useEffectAsync(async () => {
  const st = refShapeTop.current
  const tt = refTextTop.current
  const sb = refShapeBottom.current
  const tb = refTextBottom.current
  const u = refUnderline.current

  if (st && tt && sb && tb && u) {
    motion.run(async () => {
      await motion.to(st, 1, 'out', { scaleX: '1' })
      motion.set(tt, { opacity: '1' })
      motion.set(st, { transformOrigin: 'right center' })
      await motion.to(st, 1, 'out', { scaleX: '0' })
    })
    await motion.delay(0.2)
    motion.run(async () => {
      await motion.to(sb, 1, 'out', { scaleX: '1' })
      motion.set(tb, { opacity: '1' })
    })
  }
}, [])
  • motion.to(target, duration, easing, values)

    • 指定秒数で目的のスタイルにトランジションします。
    • targetHTMLElementcreateMotion(ID) 由来のキー列挙(例: 'BOX')を受け取ります。
    • easing: in | out | inout | bounce | linear
    • 変形は個別キーで指定します(例: translateX, rotate, scale)。複合 transform は渡さないでください。
    • 任意: options?: { signal?: AbortSignal } を渡すと、signal.abort()await motion.to(...) の待機を短絡できます(スタイル適用自体は行われます)。
  • motion.repeat(target, duration, values)

    • 同一トランジションを繰り返します。{ pause, play, stop, destroy } を返します。
    • pause(): 一時停止(内部のタイマー/RAF をキャンセル)
    • play(): 再開(短い間隔で pause()/play() をトグルしても多重ループ化しません)
    • stop()/destroy(): 完全停止+必要な場合は元の style に復帰(destroystop のエイリアス)
    • prefers-reduced-motion は利用側でガードしてください(例: if (matchMedia('(prefers-reduced-motion: reduce)').matches) return
  • motion.get(target, property)

    • 計算後スタイルを取得します。
  • motion.delay(seconds)

    • seconds 秒待機する Promise<void>
  • useEffectAsync(effect, deps)

    • async/await をそのまま書ける useEffect の薄いラッパー。
    • cleanup は effect の戻り値ではなく、第一引数 c で登録します。
    • c は同期的に登録されるため、await 中に unmount されても登録済み cleanup は確実に実行されます。
useEffectAsync(async (c) => {
  const id = window.setInterval(() => {
    // do something
  }, 1000)
  c(() => window.clearInterval(id))

  await motion.delay(1)
}, [])

注意(Client-only)

  • createId は RSC から参照可能ですが、createMotion/motion.*/ID.*.E() はブラウザ専用です。
  • ブラウザ環境でない場合にクライアント API を呼び出すと例外を投げます。クライアント用モジュールには 'use client' を付与してください。

About

No description, website, or topics provided.

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages