SvelteKit でのスクロリーテリング
スクロールに連動して背景のビジュアル(写真・動画・地図・グラフ)が切り替わる物語型記事を、SvelteKit 5 と Svelte 5 の runes で構築するための包括的な手引き。
対象読者の注意:このスキルは技術者・データ可視化担当向けです。SvelteKit/Svelte、TypeScript、CSS の実装知識を前提とします。コードを書かない記者・編集者には難度が高いため、企画段階(どの場面で何を見せ、どのテキストを重ねるか)を記者が設計し、実装はフロントエンド担当と分担することを想定しています。記者が単独で完成させるためのツールではありません。
注意:本スキルは一般的な実装手引きであり、特定プロジェクトの動作保証ではありません。コンポーネントの実体は references/ 配下にあります(後述)。フレームワークや依存ライブラリのバージョンは更新が早いため、導入前に各ライブラリの最新ドキュメントを確認してください。
クイックスタート
<script>
import ScrollySection from '$lib/components/scrolly/ScrollySection.svelte';
import MyVisualization from './MyVisualization.svelte';
let activeStep = $state(0);
const steps = [
{ text: '1つ目の語り(ナラティブ)' },
{ text: '2つ目の語り' },
{ text: '3つ目の語り' }
];
</script>
<ScrollySection bind:activeStep={activeStep} {steps}>
{#snippet children({ activeStep })}
<MyVisualization {activeStep} />
{/snippet}
</ScrollySection>
中核となる設計
スクロリーのパターン
スクロリーテリングは、貼り付き(sticky)背景+スクロールする前景というパターンで成り立つ。
┌─────────────────────────────────┐
│ 貼り付くビジュアル層 │ ← position: sticky, z-index: 1
│ (画像/動画/地図/グラフ) │ テキストがスクロールしても固定される
│ │
│ ┌─────────────────────────┐ │
│ │ テキストボックス(②) │ │ ← テキスト列がビジュアルに重なる
│ └─────────────────────────┘ │ z-index: 2
│ │
└─────────────────────────────────┘
要点:ビジュアル層を position: sticky で固定し、その上をテキスト列が通り過ぎていくことで、「スクロールするとビジュアルが切り替わる」という錯覚を作る。
コンポーネント・リファレンス
以下で示すコンポーネントの実体(.svelte 7本)は references/ ディレクトリに収録してある。新規プロジェクトでは、これらを $lib/components/scrolly/(または $lib/components/cleared/)にコピーして使う。本文では原典に合わせて $lib/... のパスで記載するが、参照元の実装は references/<コンポーネント名>.svelte を見ること。
ScrollySection
貼り付き/スクロールの関係を管理する中心的なコンテナ。
実装:references/ScrollySection.svelte(配置先の例:$lib/components/scrolly/ScrollySection.svelte)
Props
| Prop | 型 | 既定値 | 説明 |
|---|
activeStep | number | 0 | バインド可。現在のステップ番号 |
steps | Step[] | 必須 | 各ステップの内容の配列 |
backgroundColor | string | '#0a0a0a' | 背景色 |
showTextBoxes | boolean | true | ステップごとのテキストボックスを表示するか |
textBoxVariant | 'light' | 'dark' | 'light' | テキストボックスの配色 |
textBoxPosition | 'center' | 'left' | 'right' | 'center' | テキストボックスの横位置 |
firstStepOffset | number | 0 | 最初のステップを下へ押し下げるビューポート比率 |
onStepEnter | (step, direction) => void | - | ステップが変わったときのコールバック |
onScrollProgress | (progress) => void | - | スクロール進捗(0〜1)を返すコールバック |
children | Snippet<[{ activeStep }]> | - | ビジュアル表示用のスニペット |
Step インターフェース
interface Step {
text?: string; // テキストボックスに入れる HTML
title?: string; // 任意のタイトル
image?: string; // テキストボックスに表示する画像 URL
bgColor?: string; // 個別の背景色
raw?: boolean; // true ならラッパーなしで text をそのまま描画
source?: { text: string; url: string }; // 出典表記
imageCredit?: string; // 写真クレジット
}
使用例
<ScrollySection
bind:activeStep={heroStep}
steps={heroSteps}
backgroundColor="#000000"
showTextBoxes={true}
textBoxVariant="light"
onScrollProgress={(p) => (scrollProgress = p)}
>
{#snippet children({ activeStep })}
<HeroVisualization currentImage={heroImages[activeStep]} />
{/snippet}
</ScrollySection>
ScrollyHelper
IntersectionObserver を使ったステップ追跡コンポーネント。どのステップが最も見えているかを判定する。
実装:references/ScrollyHelper.svelte(配置先の例:$lib/components/scrolly/ScrollyHelper.svelte)
仕組み
- 子要素(各ステップ)を包む
- 子要素ごとに IntersectionObserver を作る
- 各ステップの交差比率(intersection ratio)を追跡する
- 最も見えているステップの番号を
value バインディングとして公開する
- どのステップも見えていないときは
undefined を返す
Props
| Prop | 型 | 既定値 | 説明 |
|---|
value | number | undefined | - | バインド可。現在のステップ番号 |
root | Element | null | null | 交差判定の基準(root) |
top | number | 0 | 上マージン(ピクセル) |
bottom | number | 0 | 下マージン(ピクセル) |
increments | number | 100 | しきい値の粒度 |
使い方
<script>
let currentStep = $state(undefined);
</script>
<ScrollyHelper bind:value={currentStep} top={200} bottom={200}>
<div class="step">ステップ0の内容</div>
<div class="step">ステップ1の内容</div>
<div class="step">ステップ2の内容</div>
</ScrollyHelper>
ScrollyTextBox
語り(ナラティブ)の本文を表示するためのスタイル付きテキストボックス。
実装:references/ScrollyTextBox.svelte(配置先の例:$lib/components/scrolly/ScrollyTextBox.svelte)
Props
| Prop | 型 | 既定値 | 説明 |
|---|
title | string | '' | ボックスのタイトル |
image | string | null | null | 画像 URL |
bgColor | string | null | null | 個別の背景色 |
active | boolean | true | アクティブ状態か |
variant | 'light' | 'dark' | 'light' | 配色 |
maxWidth | 'narrow' | 'wide' | 'narrow' | ボックスの幅 |
source | { text, url } | null | null | 出典表記 |
imageCredit | string | null | null | 写真クレジット |
children | Snippet | - | 内容スロット |
スタイル
- light(明):白背景+ぼかし(blur)
- dark(暗):ほぼ黒の背景+ぼかし
- アクティブ時:不透明度100%、
translateY(0)
- 非アクティブ時:不透明度75%、
translateY(8px)
/* アクティブへの遷移 */
.scrolly-text-box {
opacity: 0.75;
transform: translateY(8px);
transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.scrolly-text-box.active {
opacity: 1;
transform: translateY(0);
}
ビジュアル表示コンポーネント
画像スクロリー(HeroVisualization)
フェード効果つきのシンプルな画像切り替え。
実装:references/HeroVisualization.svelte(配置先の例:$lib/components/cleared/HeroVisualization.svelte)
<script lang="ts">
interface Props {
currentImage: string;
fadeProgress?: number; // 0〜1
}
let { currentImage, fadeProgress = 0 }: Props = $props();
</script>
<div class="hero-visualization">
<img src={currentImage} alt="場面" class="hero-image" />
<div class="hero-overlay"></div>
{#if fadeProgress > 0}
<div class="fade-overlay" style:opacity={fadeProgress}></div>
{/if}
</div>
<style>
.hero-visualization {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
background: #000;
}
.hero-image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.3) 50%,
rgba(0, 0, 0, 0.5) 100%
);
}
.fade-overlay {
position: absolute;
inset: 0;
background: #0a0a0a;
pointer-events: none;
}
</style>
親コンポーネントでの使い方
<script>
let heroStep = $state(0);
let heroScrollProgress = $state(0);
const heroImages = [
'/images/scene1.jpg',
'/images/scene2.jpg',
'/images/scene3.jpg'
];
let currentHeroImage = $derived(heroImages[heroStep] ?? heroImages[0]);
// 最後のステップで黒へフェード
let heroFadeProgress = $derived(() => {
if (heroStep !== 2) return 0;
const fadeStart = 0.75;
if (heroScrollProgress < fadeStart) return 0;
return Math.min(1, (heroScrollProgress - fadeStart) / (1 - fadeStart));
});
</script>
<ScrollySection
bind:activeStep={heroStep}
steps={heroSteps}
onScrollProgress={(p) => (heroScrollProgress = p)}
>
{#snippet children({ activeStep })}
<HeroVisualization
currentImage={currentHeroImage}
fadeProgress={heroFadeProgress()}
/>
{/snippet}
</ScrollySection>
動画スクロリー(VideoScrollyVisualization)
ステップごとに切り替える複数動画の表示。
実装:references/VideoScrollyVisualization.svelte(配置先の例:$lib/components/scrolly/VideoScrollyVisualization.svelte)
<script lang="ts">
interface VideoStep {
videoSrc: string;
poster?: string;
}
interface Props {
activeStep: number;
videoSteps: VideoStep[];
}
let { activeStep, videoSteps }: Props = $props();
let videoRefs: HTMLVideoElement[] = $state([]);
$effect(() => {
// すべて一時停止し、アクティブな動画だけ再生する
videoRefs.forEach((video, i) => {
if (video) {
if (i === activeStep) {
video.play().catch(() => {});
} else {
video.pause();
video.currentTime = 0;
}
}
});
});
</script>
<div class="video-scrolly-viz">
<div class="video-frame">
{#each videoSteps as step, i}
<video
src={step.videoSrc}
poster={step.poster}
bind:this={videoRefs[i]}
class="video-layer"
class:active={i === activeStep}
muted
loop
playsinline
preload="metadata"
/>
{/each}
</div>
<div class="video-vignette"></div>
</div>
<style>
.video-scrolly-viz {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
background: #000;
}
.video-frame {
position: absolute;
inset: 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.video-layer {
position: absolute;
width: 100%;
height: 100%;
object-fit: contain;
opacity: 0;
transition: opacity 500ms ease;
pointer-events: none;
}
.video-layer.active {
opacity: 1;
}
.video-vignette {
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.6) 100%);
pointer-events: none;
}
</style>
自動再生の注意:多くのブラウザは音声付き動画の自動再生をブロックする。スクロール連動で自動再生するには muted(消音)と playsinline(インライン再生)を必ず付ける。play() は Promise を返し、ブロック時に reject されるため .catch(() => {}) で握りつぶしている。
親コンポーネントでの使い方
<script>
let videoStep = $state(0);
const videoData = [
{ videoSrc: '/videos/clip1.mp4' },
{ videoSrc: '/videos/clip2.mp4' },
{ videoSrc: '/videos/clip3.mp4' }
];
const videoSteps = [
{ raw: true, text: '<div class="quote-card">引用1</div>' },
{ raw: true, text: '<div class="quote-card">引用2</div>' },
{ raw: true, text: '<div class="quote-card">引用3</div>' }
];
</script>
<ScrollySection
bind:activeStep={videoStep}
steps={videoSteps}
backgroundColor="#000000"
showTextBoxes={true}
>
{#snippet children({ activeStep })}
<VideoScrollyVisualization {activeStep} videoSteps={videoData} />
{/snippet}
</ScrollySection>
地図スクロリー(MapScrolly)
ステップ駆動でカメラ移動とレイヤー表示を切り替えるインタラクティブ地図。
実装:references/MapScrolly.svelte(配置先の例:$lib/components/cleared/MapScrolly.svelte)
主な機能
- MapTiler/Mapbox GL の連携
- ステップ間の fly-to(飛行)アニメーション
- GeoJSON レイヤーの管理
- フェードイン/フェードアウトのオーバーレイ
地図タイルの注意:MapTiler/Mapbox はいずれも API キーが必要で、利用規約・課金・帰属表示(attribution)の条件がある。日本の地図を扱う場合は、国土地理院の地理院タイルなど国内の地図ソースも選択肢になる(利用規約・出典表記の要件を必ず確認)。背景地図の出典クレジットは記事面に明記すること。
Props
| Prop | 型 | 既定値 | 説明 |
|---|
activeStep | number | 必須 | 現在のステップ番号 |
fadeProgress | number | 0 | フェードインのオーバーレイ(1=黒、0=見える) |
fadeOutProgress | number | 0 | 終端でのフェードアウト |
ステップのデータ構造
{
"steps": [
{
"coordinates": [139.6917, 35.6895],
"zoom": 7,
"pitch": 0,
"bearing": 0,
"duration": 2000,
"layers": {
"evictions": { "opacity": 0.6 },
"villages": { "visible": true, "opacity": 1.0 }
}
}
]
}
coordinates は [経度, 緯度] の順であることに注意(GeoJSON/Mapbox の慣例。緯度・経度の順ではない)。上の例は東京周辺の座標。
fly-to の実装
function flyToStep(index: number) {
if (!map || !mapReady) return;
const step = steps[index];
if (!step) return;
map.flyTo({
center: step.coordinates,
zoom: step.zoom,
bearing: step.bearing || 0,
pitch: step.pitch || 0,
duration: step.duration || 2000,
easing: (t: number) => t * (2 - t) // ease-out quad(終わりが滑らかな減速)
});
updateLayerVisibility(index);
}
文書モザイク(NoticeMosaic)
書類を散らして配置し、段階的に見せていく表示。立ち退き通知や告示・公文書などを「証拠の山」として演出する用途に向く。
実装:references/NoticeMosaic.svelte(配置先の例:$lib/components/scrolly/NoticeMosaic.svelte)
<script lang="ts">
interface Notice {
image: string;
alt: string;
title: string;
subtitle: string;
excerpt: string;
}
interface Props {
activeStep: number;
notices: Notice[];
backgroundColor?: string;
}
let { activeStep, notices, backgroundColor = '#1a1715' }: Props = $props();
let currentNoticeIndex = $derived(Math.min(activeStep, notices.length - 1));
// ばらつき(jitter)を加えた決定論的な配置
function getPosition(index: number): [string, string, number] {
const positions: [number, number, number][] = [
[20, 15, -3],
[50, 25, 2.5],
[15, 45, -1.5],
[55, 10, 3]
];
const pos = positions[index % positions.length];
const seed = index * 7919;
const jitterX = ((seed % 10) - 5) * 0.5;
const jitterY = (((seed * 3) % 10) - 5) * 0.5;
return [`${pos[0] + jitterX}%`, `${pos[1] + jitterY}%`, pos[2]];
}
</script>
配置を Math.random() ではなく index から導く「決定論的(deterministic)」な計算にしているのは、再描画やサーバーサイドレンダリング(SSR)でも位置がぶれないようにするため。乱数だと毎回レイアウトが変わり、ハイドレーションのずれや見た目のちらつきの原因になる。
重要な CSS ルール
オーバーレイに position: fixed を使わない
問題:fixed のオーバーレイはビューポート全体(その下にある別セクションも含む)を覆ってしまう。
/* 悪い例 — ページ全体が暗くなる */
.fade-overlay {
position: fixed;
inset: 0;
background: #0a0a0a;
z-index: 5;
}
/* 良い例 — 親コンテナの内側に限定される */
.fade-overlay {
position: absolute;
inset: 0;
background: #0a0a0a;
pointer-events: none;
}
z-index の指針
z-index は単純な値に保ち、スコープを限定する。
| 層 | z-index | 説明 |
|---|
| ビジュアル層(sticky) | 1 | 背景のビジュアル |
| テキスト列 | 2 | スクロールする本文 |
| フェードのオーバーレイ | 10 | 遷移演出(セクション内部) |
| フッター | 10 | フッターを確実に見せる |
ScrollySection に隣り合うセクションに大きな z-index を付けないこと。
<!-- 悪い例 — スクロリーの上に重なってしまう -->
<section class="intro relative z-10">
<!-- 良い例 — 自然な重なり順 -->
<section class="intro relative">
貼り付き(sticky)配置の要件
position: sticky を効かせるには次が必要。
-
overflow: hidden ではなく overflow-x: clip を使う:
:global(html), :global(body) {
overflow-x: clip; /* sticky を壊さない */
}
-
スクロリーのコンテナに isolation を付ける:
.scroll-section {
position: relative;
isolation: isolate;
}
-
ビジュアル層の構造:
.visual-layer {
position: sticky;
top: 0;
width: 100%;
height: 100vh;
z-index: 1;
}
スクロリー以外のコンテンツセクション
スクロールロックを伴わない、通常の記事本文の作り方。
コンテンツセクションの構造
<section class="content-section">
<div class="content-container">
<h2 class="content-heading">セクション見出し</h2>
<div class="prose-content">
<p>記事本文…</p>
</div>
</div>
</section>
CSS
/* 暗いテーマのコンテンツセクション */
.content-section {
position: relative;
background: #0a0a0a;
padding: 5rem 1.5rem;
}
.content-container {
max-width: 42rem;
margin: 0 auto;
}
.content-heading {
font-family: 'Playfair Display', Georgia, serif;
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-weight: 600;
line-height: 1.2;
color: #ffffff;
margin: 0 0 2rem 0;
}
.prose-content {
font-family: 'Source Sans 3', system-ui, sans-serif;
font-size: 1.125rem;
line-height: 1.85;
color: rgba(255, 255, 255, 0.85);
}
.prose-content p {
margin: 0 0 1.5rem 0;
}
.prose-content strong {
font-weight: 600;
color: #ffffff;
}
.prose-content blockquote {
border-left: 4px solid rgba(255, 255, 255, 0.2);
padding-left: 1.5rem;
margin: 2rem 0;
font-style: italic;
color: rgba(255, 255, 255, 0.6);
}
日本語フォントの注意:上の Playfair Display/Source Sans 3 は欧文向けのフォント。日本語本文では Noto Sans JP・Noto Serif JP・游ゴシック/游明朝などの和文フォントを併せて指定する。明朝体(serif)は格調・重厚さ、ゴシック体(sans-serif)は可読性に向く。本文の行間(line-height)は和文では 1.8 前後がやや広めで読みやすい。縦書きを使う場合は writing-mode: vertical-rl を検討する。
年表(タイムライン)
.timeline-events {
margin: 2rem 0;
}
.event {
padding-left: 1.5rem;
border-left: 4px solid #dc2626;
margin-bottom: 1.5rem;
}
.event-date {
font-weight: 600;
color: #ffffff;
margin: 0 0 0.25rem 0;
}
ステップのデータ設計
標準のテキストステップ
const steps = [
{
title: '2021年9月23日',
text: '出来事の説明。<strong>強調</strong>もできる。',
source: { text: '共同通信', url: 'https://...' },
imageCredit: '撮影者名'
}
];
生 HTML のステップ(カスタムカード)
const steps = [
{
raw: true,
text: `
<div class="custom-card">
<span class="date">2025年8月4日</span>
<blockquote>「ここに引用文」</blockquote>
<a href="..." class="source-link">出典を見る</a>
</div>
`
}
];
ヒーロー見出し(最初のステップ)
const heroSteps = [
{
raw: true,
text: `
<div class="hero-header">
<h1 class="hero-title">記事タイトル</h1>
<p class="hero-desc">サブタイトルの説明</p>
<div class="hero-byline">
<p class="byline-label">取材・執筆</p>
<p class="byline-authors">執筆者名</p>
<p class="byline-date">2026年1月</p>
</div>
</div>
`
},
// …以降のステップが続く
];
フェード演出
セクション末尾で黒へフェード
let scrollProgress = $state(0);
// フェード計算:スクロール75%地点で開始し、100%で完了
let fadeProgress = $derived(() => {
const fadeStart = 0.75;
if (scrollProgress < fadeStart) return 0;
return Math.min(1, (scrollProgress - fadeStart) / (1 - fadeStart));
});
セクション冒頭でフェードイン
// 最初の25%で、黒(1)から見える状態(0)へ
let fadeInProgress = $derived(() => {
const fadeEnd = 0.25;
if (scrollProgress >= fadeEnd) return 0;
return 1 - (scrollProgress / fadeEnd);
});
フェードのオーバーレイを当てる
{#if fadeProgress > 0}
<div class="fade-overlay" style:opacity={fadeProgress}></div>
{/if}
<style>
.fade-overlay {
position: absolute;
inset: 0;
background: #0a0a0a;
pointer-events: none;
z-index: 10;
}
</style>
ページ全体の構成
<div class="article-container">
<!-- ヒーロー(画像)スクロリー -->
<ScrollySection bind:activeStep={heroStep} steps={heroSteps}>
{#snippet children({ activeStep })}
<HeroVisualization ... />
{/snippet}
</ScrollySection>
<!-- データ可視化スクロリー -->
<ScrollySection bind:activeStep={dataStep} steps={dataSteps}>
{#snippet children({ activeStep })}
<DataVisualization ... />
{/snippet}
</ScrollySection>
<!-- 通常のコンテンツセクション -->
<section class="content-section">
<div class="content-container">
<h2>セクション見出し</h2>
<div class="prose-content">…</div>
</div>
</section>
<!-- 地図スクロリー -->
<ScrollySection bind:activeStep={mapStep} steps={mapSteps}>
{#snippet children({ activeStep })}
<MapScrolly ... />
{/snippet}
</ScrollySection>
<!-- フッター -->
<footer class="article-footer">…</footer>
</div>
<style>
.article-container {
background: #0a0a0a;
min-height: 100vh;
}
:global(html), :global(body) {
overflow-x: clip;
}
</style>
トラブルシューティング
動画スクロリーが表示されない
- 周囲のセクションの z-index を確認する(
z-10 を外す)
- ビューポートを覆う
position: fixed のオーバーレイがないか確認する
- 指定したパスに動画ファイルが実在するか確認する
暗いオーバーレイがページ全体を覆う
原因:フェードのオーバーレイが absolute ではなく fixed になっている。
対処:
.fade-overlay {
position: absolute; /* fixed にしない! */
inset: 0;
}
sticky が効かない
- 祖先要素に
overflow: hidden がないか確認する(overflow-x: clip を使う)
position: sticky に top: 0 があるか確認する
- コンテナに高さ、または十分な内容があるか確認する
ステップが切り替わらない
- ステップ要素に十分な高さがあるか確認する(
min-height: 70vh など)
- IntersectionObserver のマージン設定を確認する
- ステップが ScrollyHelper の直接の子になっているか確認する
タイポグラフィの指針
/* 見出し(欧文) */
font-family: 'Playfair Display', Georgia, serif;
/* 本文(欧文) */
font-family: 'Source Sans 3', system-ui, sans-serif;
/* 等幅・日付 */
font-family: 'JetBrains Mono', 'Courier New', monospace;
日本語記事では和文フォントを併記する(例):
/* 見出し(和文・明朝) */
font-family: 'Playfair Display', 'Noto Serif JP', 'Yu Mincho', Georgia, serif;
/* 本文(和文・ゴシック) */
font-family: 'Source Sans 3', 'Noto Sans JP', 'Yu Gothic', system-ui, sans-serif;
フォントの読み込み
<svelte:head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Playfair+Display:wght@400;500;600;700&family=Source+Sans+3:wght@300;400;500;600&display=swap"
rel="stylesheet"
/>
<!-- 和文フォントを使う場合は Noto Sans JP / Noto Serif JP も併せて読み込む。
和文は容量が大きいため、必要なウェイトだけに絞ると表示が速くなる -->
</svelte:head>
依存ライブラリ
{
"dependencies": {
"svelte": "^5.0.0",
"@sveltejs/kit": "^2.0.0"
}
}
特定のビジュアルで任意に追加:
- 地図:
mapbox-gl または @maptiler/sdk
- データ可視化:
d3
- 複雑なアニメーション:
gsap
アクセシビリティ・公開前の確認
スクロリーテリングは演出が強いぶん、読者によっては使いにくくなりやすい。公開前に次を確認する。
- キーボード・スクリーンリーダー:スクロール連動の情報が、本文テキストとしても順に読めること。ビジュアルの切り替えだけに意味を持たせず、テキストで内容が完結するようにする。
- モーション設定の尊重:
prefers-reduced-motion を尊重し、fly-to や激しいフェードを抑える分岐を入れる。
- モバイル:縦長画面・低速回線での挙動を確認する。動画・地図タイルは通信量が大きいので、
preload の設定や画像の最適化を検討する。
- 代替テキスト:画像の
alt、地図・グラフの説明を必ず付ける。
- 出典・クレジット:写真・動画・地図・データの出典を記事面に明示する(
source・imageCredit を活用)。地図タイルの帰属表示は規約上の義務であることが多い。
出典・ライセンス(帰属表示)
本スキルは AI Journalism Skills(by Florent "Flo" Daudens, CC BY 4.0,
https://huggingface.co/spaces/fdaudens/ai-journalism-skills )の scrolly-sveltekit スキルを
原典の構成(設計思想・コンポーネントリファレンス・CSS の落とし穴・ステップ設計・フェード演出・トラブルシューティング・付属の .svelte コンポーネント)に沿って日本語化し、
地図ソース・フォント・出典表記など一部を日本の報道実務(地理院タイル、和文フォント、出典クレジット、アクセシビリティ)向けに補足・再構成したものです。
コンポーネントの実体(.svelte 7本)は references/ に収録しています。コードは機能を保持し、コメントのみ日本語化しています。
原典の著作物本文(第三者の素材)は転載していません。改変を加えており、原作者の公認・推奨を受けたものではありません。
ライセンス(CC BY 4.0)の全文は https://creativecommons.org/licenses/by/4.0/ を参照。