M-020 Animation moderate

Scroll Progress & Section Indicator スクロール進行バー&セクションインジケーター

Header reading progress bar synced with active section tracking and scroll completion percentage. スクロール量と現在閲覧中のセクションをヘッダーバーと連動させるアクセシブルな読了進行インジケーター。

ライブデモ Live Demo

読了状況Reading Progress Sec 1
0%

1 はじめに:設計思想1. Design Philosophy

2 アクセシビリティと配慮2. Accessibility Details

3 カスタマイズと展開3. Customization & Layouts

4 まとめと実装のポイント4. Summary & Takeaways

概要・用途・特徴Overview, Usage & Features

何ができるかWhat it does

A sticky header reading progress bar paired with real-time percentage updates and dynamic active section tracking.

スクロール領域内の現在位置から読了率(0〜100%)を算出し、ヘッダー直下のプログレスバーと現在セクションバッジを連動更新するUIです。

どこで使うかWhere to use

Used in long documentation, editorial content, tutorials, and modal scroll areas to improve orientation.

長文ドキュメント、技術ブログ、利用規約同意モーダルなど、全体の長さを把握させたい領域で幅広く活用できます。

特徴Key features

Smooth width transitions, active section detection, percentage readout, CSS variable theme integration, accessible structure.

滑らかな幅の補間、現在位置に応じたセクションのハイライト検知、パーセンテージ表示、CSS変数によるテーマ変更対応。

調整可能パラメータ Adjustable Parameters

Parameter Default Description
bar-color#38bdf8Progress bar fill and glow color
bar-height4pxThickness of the sticky progress line

実装コード Implementation Code

<div class="m020-stage">
  <div class="m020-header">
    <div class="m020-title">Reading Progress <span class="m020-badge" id="m020-badge">Sec 1</span></div>
    <div class="m020-pct" id="m020-pct">0%</div>
    <div class="m020-bar-track"><div class="m020-bar-fill" id="m020-fill"></div></div>
  </div>
  <div class="m020-scroll-area" id="m020-area">
    <article class="m020-section is-active"><h4><span class="m020-sec-num">1</span> Design</h4><p>Content...</p></article>
    <article class="m020-section"><h4><span class="m020-sec-num">2</span> Accessibility</h4><p>Content...</p></article>
  </div>
</div>
.m020-stage {
  position: relative;
  width: 100%;
  max-width: 640px;
  border-radius: 16px;
  background: #090d16;
  border: 1px solid rgba(148, 163, 184, 0.2);
  overflow: hidden;
}

.m020-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: rgba(15, 23, 42, 0.88);
  backdrop-filter: blur(8px);
  padding: 12px 18px;
  display: flex;
  justify-content: space-between;
}

.m020-bar-track {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: var(--m020-bar-height, 4px);
  background: rgba(148, 163, 184, 0.12);
}

.m020-bar-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--m020-bar-color, #38bdf8), #818cf8);
  box-shadow: 0 0 10px var(--m020-bar-color, #38bdf8);
  transition: width 0.08s ease-out;
}

.m020-scroll-area {
  height: 280px;
  overflow-y: auto;
  padding: 20px 24px;
}

.m020-section {
  padding: 20px 0;
  opacity: 0.35;
  transform: translateY(12px);
  transition: opacity 0.35s ease, transform 0.35s ease;
}

.m020-section.is-active {
  opacity: 1;
  transform: translateY(0);
}
import React, { useState, useRef } from 'react';
import './M-020.css';

export default function ScrollProgressReveal({
  sections = [],
  barColor = '#38bdf8',
  barHeight = 4
}) {
  const [progress, setProgress] = useState(0);
  const [activeSec, setActiveSec] = useState(1);
  const scrollRef = useRef(null);

  const handleScroll = () => {
    const el = scrollRef.current;
    if (!el) return;
    const max = el.scrollHeight - el.clientHeight;
    const pct = max > 0 ? Math.round((el.scrollTop / max) * 100) : 0;
    setProgress(pct);

    const children = el.querySelectorAll('.m020-section');
    children.forEach((sec, idx) => {
      if (sec.offsetTop - el.scrollTop <= 120) {
        setActiveSec(idx + 1);
      }
    });
  };

  return (
    <div className="m020-stage" style={{ '--m020-bar-color': barColor, '--m020-bar-height': `${barHeight}px` }}>
      <div className="m020-header">
        <span>Reading Progress <span className="m020-badge">Sec {activeSec}</span></span>
        <span className="m020-pct">{progress}%</span>
        <div className="m020-bar-track">
          <div className="m020-bar-fill" style={{ width: `${progress}%` }} />
        </div>
      </div>
      <div className="m020-scroll-area" ref={scrollRef} onScroll={handleScroll}>
        {sections.map((sec, idx) => (
          <article key={idx} className={`m020-section ${activeSec === idx + 1 ? 'is-active' : ''}`}>
            <h4><span className="m020-sec-num">{idx + 1}</span> {sec.title}</h4>
            <p>{sec.content}</p>
          </article>
        ))}
      </div>
    </div>
  );
}
.m020-stage {
  position: relative;
  width: 100%;
  max-width: 640px;
  border-radius: 16px;
  background: #090d16;
  border: 1px solid rgba(148, 163, 184, 0.2);
  overflow: hidden;
}

.m020-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: rgba(15, 23, 42, 0.88);
  backdrop-filter: blur(8px);
  padding: 12px 18px;
  display: flex;
  justify-content: space-between;
}

.m020-bar-track {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: var(--m020-bar-height, 4px);
  background: rgba(148, 163, 184, 0.12);
}

.m020-bar-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--m020-bar-color, #38bdf8), #818cf8);
  box-shadow: 0 0 10px var(--m020-bar-color, #38bdf8);
  transition: width 0.08s ease-out;
}

.m020-scroll-area {
  height: 280px;
  overflow-y: auto;
  padding: 20px 24px;
}

.m020-section {
  padding: 20px 0;
  opacity: 0.35;
  transform: translateY(12px);
  transition: opacity 0.35s ease, transform 0.35s ease;
}

.m020-section.is-active {
  opacity: 1;
  transform: translateY(0);
}

仕組みとカスタマイズHow It Works & Customization

仕組みHow it works

Listens to scroll events on the target element, calculates the ratio of scrollTop to scrollable distance, and toggles active classes based on section offsets.

スクロールイベントからscrollTopと最大スクロール量の比率を算出してバー幅に反映し、セクションのoffsetTopとの比較でアクティブクラスを切り替えます。

カスタマイズ方法Customization

Change bar color, height, and background gradients using custom properties --m020-bar-color and --m020-bar-height.

CSSカスタムプロパティ(--m020-bar-color, --m020-bar-height)でバーの配色や高さを自由にカスタマイズ可能です。

注意点Caveats

When binding to the window object on mobile devices, account for dynamic URL bar resizing in height calculations.

モバイルブラウザのウィンドウスクロールに適用する際は、アドレスバーの開閉による高さ変動(dvh / vh)にご注意ください。

よくある質問 Frequently Asked Questions

How can I adapt this to the full browser window instead of a container? コンテナ内ではなくブラウザウィンドウ全体のスクロールに適用するにはどうすればよいですか?

Listen to window.addEventListener('scroll') and compute progress with document.documentElement.scrollTop / (document.documentElement.scrollHeight - window.innerHeight).

windowのscrollイベントを監視し、document.documentElement.scrollTop / (scrollHeight - window.innerHeight) で全体の進捗率を計算して適用します。

Is the scroll event performant on lower-end devices? 低スペック端末でのスクロールパフォーマンスは問題ありませんか?

Yes, passive event listeners and requestAnimationFrame throttling ensure 60fps responsiveness without layout thrashing.

はい、passive: true イベントリスナーとシンプルなDOM幅更新により、レイアウト再計算を抑えて滑らかな60fpsを維持します。

AIへの指示テンプレート AI Prompt Template

以下をAIに貼り付けると実装を依頼できます。 Paste the following into your AI assistant to request implementation.