• #vue
  • #nuxt3
  • #css-transition
  • #animation
  • #react-migration
開発mdx-playgroundメモ

React TSXの掛け算スライドショーをVue.jsに移植した記録

React TSXで書かれた掛け算の面積図スライドショーを、Nuxt 3のVueページとして動かすところまで持っていった。数値の入力からスライド遷移、P3ピースの回転アニメーションまで一通り移植が完了している。途中、CSSトランジションが効かない問題に2時間ほど手を止められた。

移植の方針

元のコンポーネントはReact + Tailwind CSSで構成されていた。移植先のmdx-playgroundではTailwindを使っていないため、scoped CSSで書き直す方針を採った。

Nuxt 3のカスタムVueページとして apps/web/app/pages/multiplication-slideshow.vue に配置し、definePageMeta でメタ情報を設定した。

<script setup lang="ts">
definePageMeta({
  title: '十の位が1の2桁×2桁 速算スライドショー',
  publishedAt: '2026-03-22',
  tags: ['education', 'math'],
  includeInList: true
})
</script>

includeInList: true を指定することで、トップページの記事一覧にも表示される。

CSSトランジションが動かない

移植自体はスムーズに進んだが、P3ピースの回転アニメーションで壁にぶつかった。

症状

スライドを切り替えると、P3ピースが回転アニメーションなしで一瞬で位置が変わる。他のピース(P1, P2, P4)は transition: all 0.7s ease-in-out で滑らかに移動するのに、P3だけ瞬間移動する。

原因を追う

最初はCSSの書き方を疑った。transform: rotate(90deg) をインラインスタイルで当てているので、transition プロパティに transform が含まれているか確認した。含まれている。

次に、Vue DevToolsでDOM要素の再生成が起きていないか確認した。ここで原因が見えた。

v-for のキーに currentSlide を含めていた。

<!-- NG: スライドが変わるたびにDOM要素が再生成される -->
<div
  v-for="(piece, index) in pieces"
  :key="`piece-${index}-${currentSlide}`"
  class="piece"
>

:keycurrentSlide が入っているので、スライドを切り替えるたびにVueはDOM要素を破棄して新しく作り直す。新しいDOM要素には前の状態がないから、CSSトランジションの「from」が存在しない。結果、アニメーションなしで最終状態がいきなり描画される。

解決: 安定したキーに変更

:key をピース固有のIDに変更した。

<!-- OK: DOM要素が再利用され、CSSトランジションが効く -->
<div
  v-for="(meta, index) in piecesMeta"
  :key="meta.id"
  class="piece"
  :style="getPieceStyle(index)"
>

piecesMeta はスライドによらない静的な情報(ID、色、ラベル)を持つ配列で、キーが変わらないのでDOM要素が再利用される。スタイルの変更はリアクティブな computed で計算し、CSSトランジションが補間してくれる。

この変更を入れた瞬間、P3ピースが90度回転しながら移動するアニメーションが動いた。

P3回転の実装構造

P3ピースは初期配置では a x 10 の縦長だが、並び替え後は 10 x a の横長として上部グループに組み込まれる。CSS transform: rotate(90deg) で見た目の回転を実現し、内部テキストは逆回転で正立を保つ。

// ピースのスタイル計算(P3は回転あり)
const getPieceStyle = (index: number) => {
  const style: Record<string, string> = {
    left: `${positions.value[index].x}px`,
    top: `${positions.value[index].y}px`,
    width: `${pieceWidths.value[index] * SCALE}px`,
    height: `${pieceHeights.value[index] * SCALE}px`
  }
  if (index === 2 && currentSlide.value >= 1) {
    style.transform = 'rotate(90deg)'
  }
  return style
}

CSSは1行だけ。

.piece {
  position: absolute;
  transition: all 0.7s ease-in-out;
}

transition: allleft, top, width, height, transform をすべて補間するので、位置移動と回転が同時に滑らかに動く。

worktree環境でのpnpm install問題

今回の作業はworktreeブランチで進めていた。devコンテナ内で pnpm dev を実行しようとしたところ、2つの問題が発生した。

  1. pnpmが見つからない: devコンテナのPATHにpnpmが含まれていなかった。npm install -g pnpm で解決
  2. node_modulesの権限混在: ホスト側(vscodeユーザー)とコンテナ側(rootユーザー)で作成されたファイルが混在し、パーミッションエラーが出た。node_modules を削除して pnpm install をやり直して解決

worktreeで新しいブランチを切った場合、node_modules は共有されないので pnpm install が毎回必要になる。この注意事項をCLAUDE.mdにも追記した。

トップページへのリンク追加

移植が完了した後、トップページの「学習・クイズ」セクションにスライドショーへのリンクを追加した。/multiplication-slideshow で直接アクセスできる。

振り返り

:key の設計ミスでCSSトランジションが無効化されるパターンは、Reactの key propでも同じことが起きる。「アニメーションさせたいならDOM要素を再生成しない」という原則は、フレームワークを問わず共通している。

今回の移植でReactとVueの差異を改めて体感した。Reactでは useState + JSXで書いていた部分が、Vueでは ref + computed + テンプレートに分かれる。ロジックの分離という点ではVueの方が構造が見えやすいと感じた。一方でJSXの自由度が恋しくなる場面もあり、どちらが良いとは言い切れない。