• #UI設計
  • #Vue
  • #IntersectionObserver
  • #レスポンシブ
  • #スライドUI
開発eurekapu-nuxt4メモ

教育コンテンツUIを抜本改善 - Theater型・Scrollytelling・ミラーカラムレイアウト

背景

教育コンテンツ用のSlideViewerが幅860pxに押し込められていて、テキストと画像が窮屈に並んでいた。教科書のページをそのまま画面に貼り付けたような見た目で、スクロールしても「どこを読んでいるか」を見失う。レイアウトを根本から見直すことにした。

やったこと

3パターンのレイアウトを実装して、実際に教材コンテンツを流し込んで比較した。

1. Theater型(採用)

テキストを上、画像を下に配置するスライドデッキ風UI。左右の矢印オーバーレイでスライドを送る。

<template>
  <div class="theater-viewer">
    <!-- キャプション: 3行分の高さを固定して画像のズレを防止 -->
    <div class="caption" :style="{ minHeight: '4.5em' }">
      {{ currentSlide.text }}
    </div>
    <!-- 画像エリア + 左右矢印オーバーレイ -->
    <div class="image-area">
      <button class="nav-arrow left" @click="prev">&#8249;</button>
      <img :src="currentSlide.image" />
      <button class="nav-arrow right" @click="next">&#8250;</button>
    </div>
    <!-- ページインジケーター(1枚スライドでは非表示) -->
    <div v-if="slides.length > 1" class="indicator">
      {{ currentIndex + 1 }} / {{ slides.length }}
    </div>
  </div>
</template>

設計で気をつけた点:

  • キャプション高さの固定: テキスト量がスライドごとに異なるため、高さを minHeight: '4.5em'(3行分)で固定した。これを入れないと、テキストの行数が変わるたびに画像がガクガク上下に動く
  • 白ベースのデザイン: 教材コンテンツなので、背景白・影なし・余白広めにして、教科書を読んでいる感覚に近づけた
  • 1枚スライドの処理: スライドが1枚しかない場合、ページ番号・矢印・コントロール一式を v-if で非表示にする。1/1と表示されても意味がない

2. Scrollytelling型(実装して比較用に残す)

左にテキスト、右に画像の2カラム構成。スクロールに連動して右側の画像が切り替わる。

// IntersectionObserverで「今どのテキストカードが画面中央にいるか」を検知
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        activeCard.value = Number(entry.target.dataset.index)
      }
    })
  },
  { rootMargin: '-40% 0px -40% 0px' }
)

2つのバグを潰した:

  • activeCardが0→2に飛ぶ問題: rootMargin の設定が広すぎて、複数のカードが同時に isIntersecting になっていた。-40% 0px -40% 0px に絞ることで、画面中央付近のカードだけを検知するようにした
  • セクション末尾の余白問題: 最後のテキストカードをスクロールしきると、右の画像が宙に浮く。最後のカードに margin-bottom を追加して、画像がビューポート内に留まるよう調整した

レスポンシブ対応

モバイル幅(768px以下)では2カラムが収まらないため、インターリーブ表示に切り替えた。テキスト→画像→テキスト→画像の順に1カラムで積む。IntersectionObserverはモバイルでは無効にして、単純なスクロールに戻す。

3. Stepper型(作成後に不採用で削除)

ステップバーを上部に置いて、クリックで切り替える方式。作ってみたが、教材コンテンツの「流れ」を表現するには向いていなかった。ステップ数が多いと横幅を食い過ぎるのと、前後の文脈が見えなくなる。実装後すぐ削除した。

Theater型の採用とコンポーネント化

3パターンを並べて教材を流し込んだ結果、Theater型を採用した。

  • テキストと画像の対応関係が一番わかりやすい
  • 矢印操作が直感的で、教材を「めくる」感覚に近い
  • キャプション固定により、画像位置がぶれない

コンポーネント TheaterViewer.vue として切り出し、ページ本体を書き換えた。

743行 → 140行に削減。3パターンの比較用コードとインラインのスタイル定義を丸ごと消して、TheaterViewerにpropsでスライドデータを渡すだけの構成にした。

<!-- ページ側: TheaterViewerを呼ぶだけ -->
<TheaterViewer :slides="categorySlides" />

ミラーカラムレイアウト

大カテゴリ・中カテゴリ・小カテゴリの3カラム構造とTheater型を組み合わせた。左カラムでカテゴリを選択すると、中央カラムにサブカテゴリが表示され、右カラムにTheaterViewerが展開する。

教科書の「目次→章→節→本文」という階層を画面上で再現する構造。3カラムの幅比率は 1:1.5:3 にして、右のTheaterViewerに十分な表示領域を確保した。

振り返り

3パターン作って比較したことで、「なぜTheater型がいいのか」を自分の手で確認できた。Scrollytellingは見た目のインパクトがあるが、IntersectionObserverのチューニングに時間を取られる。教材コンテンツでは「前後のスライドに自分のペースで移動できる」ことのほうが重要で、Theater型の矢印操作がそれに合っていた。

743行が140行に縮んだとき、画面を二度見した。3パターン分の実験コードが全部消えて、TheaterViewerのpropsだけが残っている。コンポーネント化の効果を数字で体感した瞬間だった。

IntersectionObserverの rootMargin 調整は地味だが学びがあった。「-40% 0px -40% 0px」で画面中央20%だけを検知領域にするテクニックは、スクロール連動UIでまた使う場面がありそうだ。