- アンカーリンクは、単にボタンをクリックして見出しまでスクロールするという構造だが、実装時には実際に問題が発生する
- 下部にある見出しはビューポート上端へ正確にスクロールされないため、UX の低下が起きる
- これを解決するためにさまざまなアプローチを試し、次第により精巧で複雑な方式へ発展させていく
単純な解決策: パディングを追加
- 下側の見出しがスクロールで届くように余白を追加する方式
- デルタを計算して padding を追加すれば解決できる
- ただし、デザインチームは不要な余白を嫌がるかもしれない
実用的な解決策: トリガーラインの移動
- トリガーラインをビューポート下側へ移動し、下部の見出しがそこに届くよう調整する
- 問題は、見出しがビューポートの最下部に位置してしまい、可読性が落ちること
改善案: 仮想トリガーポイントの生成
- 実際の見出し位置はそのままにして、トリガーが発生する位置だけを上側へ移動した仮想位置を作る
- 各見出しごとに異なる調整を適用できる柔軟性を確保
- ただし最初の見出しは上に移動しすぎて新たな問題が発生する → 個別調整が必要
より良い方法: トリガー位置の比例移動
- すべてのトリガーを同じだけ動かすのではなく、最初の見出しはそのまま、最後の見出しは最大限に移動
- 中間の見出しは位置に応じて比例して移動する
- 見出しの順序を維持し、スクロールで到達可能であることを保証するという条件を満たす
- この方式はシンプルで実用的であり、ほとんどの場合にうまく機能する
高度なアプローチ: カスタムマッピング関数による最適化
- トリガー位置を任意に 25% の位置へ置いたため、仮想位置が元の位置から離れすぎる可能性がある
- これを解決するために、MSE(Mean Squared Error) を使った最適化アプローチを導入
損失関数の構成
- Anchor Penalty: 仮想見出し位置が元の位置からどれだけずれたか
- Section Penalty: セクション間の距離(スクロール長)がどれだけ変化したか
- この 2 つの値を重み付けして最適なトリガー位置を導き出す
制約条件
- ページ範囲内に収める
- 最初の見出しは上へ移動しない
- 見出しの順序を保持する
インサイト: 単純な比例移動の限界
- 非常に長いページ(例: 聖書全体)では、ページ全体にわたって小さな移動を累積的に適用しなければならない非効率が生じる
- ページが長いほど誤差が大きくなり、UX に悪影響を与える可能性がある
最終的な解決策: smoothstep ベースの可変マッピング関数
- 各見出しの位置を 0〜1 の値に正規化し、それを基に調整率を計算する
- Smoothstep 関数(S(x) = 3x² - 2x³) を使って滑らかな遷移を実現
- 調整開始位置
a を設定し、その位置までは移動せず、それ以降で滑らかに増加させる
- 例:
a = 0.4 なら上位 40% の見出しは移動なし、下位 60% は段階的に調整
- 結果として、上部の見出しは元の位置を維持し、下部の見出しには最大調整を適用 → 自然な UX を提供
検証とまとめ
- 最終実装は、設計としての精巧さと実用性のバランスを備えたソリューション
- もちろん、デザイナーからのフィードバックは「…とにかくちゃんと動けばいいかも」かもしれない
- それでも少なくとも、このブログ記事は永遠に記憶される精巧なエンジニアリングの記録として残る
1件のコメント
Hacker Newsのコメント
バックエンド開発者としてフロントエンドの作業を見ると、ときどきその複雑さに驚かされる
サイドナビゲーションの「アクティブなアンカー」表示のUX目的についての疑問
アンカーリンクの最も重要なUX機能は、他の人に送れてブックマークとして保存できることであるべき
Jiraのアンカー/パーマリンクにうんざりしてクリックしたが、似ているようで別の方式だった
メインページのコンテンツ下にパディングを追加するのが理想的
現代のブラウザでは、テキストフラグメントを使ってページ内の特定部分を強調できる
複数の「アクティブ」状態を許可することも可能
他のコメントを読むのが面白い
Firefoxデスクトップでは、「美しいソリューション」が「中間セクション」を強調する
記事はすっきりしていて、ブログデザインのほうがさらに興味深い