跳到正文
cccskills
免费在 GitHub 公开

scrolly-sveltekit

SvelteKit 5 と Svelte 5 の runes を使って、スクロール連動型のビジュアル記事(スクロリーテリング)を構築する。画像スクロリー、動画スクロリー、地図スクロリー、文書モザイクの各パターン用コンポーネントと、貼り付き(sticky)背景+スクロール前景の設計、CSS の落とし穴、ステップ設計、フェード演出までを扱う。

「スクロリーテリングの記事を作りたい」「スクロールで地図や写真が切り替わる特集を組みたい」などの開発依頼で使う。

中文简介正在准备中。当前显示原文说明。

sgbms1/ja-news-skills星标 3(GitHub 3 + cccskills 0)

实测评分 未实测

查看安装方法

包含的文件(8)

  • SKILL.md29.7 KB
  • references/HeroVisualization.svelte1.1 KB
  • references/MapScrolly.svelte9.0 KB
  • references/NoticeMosaic.svelte5.4 KB
  • references/ScrollyHelper.svelte2.7 KB
  • references/ScrollySection.svelte7.0 KB
  • references/ScrollyTextBox.svelte4.9 KB
  • references/VideoScrollyVisualization.svelte2.0 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

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型既定値説明
activeStepnumber0バインド可。現在のステップ番号
stepsStep[]必須各ステップの内容の配列
backgroundColorstring'#0a0a0a'背景色
showTextBoxesbooleantrueステップごとのテキストボックスを表示するか
textBoxVariant'light' | 'dark''light'テキストボックスの配色
textBoxPosition'center' | 'left' | 'right''center'テキストボックスの横位置
firstStepOffsetnumber0最初のステップを下へ押し下げるビューポート比率
onStepEnter(step, direction) => void-ステップが変わったときのコールバック
onScrollProgress(progress) => void-スクロール進捗(0〜1)を返すコールバック
childrenSnippet<[{ 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)

仕組み

  1. 子要素(各ステップ)を包む
  2. 子要素ごとに IntersectionObserver を作る
  3. 各ステップの交差比率(intersection ratio)を追跡する
  4. 最も見えているステップの番号を value バインディングとして公開する
  5. どのステップも見えていないときは undefined を返す

Props

Prop型既定値説明
valuenumber | undefined-バインド可。現在のステップ番号
rootElement | nullnull交差判定の基準(root)
topnumber0上マージン(ピクセル)
bottomnumber0下マージン(ピクセル)
incrementsnumber100しきい値の粒度

使い方

<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型既定値説明
titlestring''ボックスのタイトル
imagestring | nullnull画像 URL
bgColorstring | nullnull個別の背景色
activebooleantrueアクティブ状態か
variant'light' | 'dark''light'配色
maxWidth'narrow' | 'wide''narrow'ボックスの幅
source{ text, url } | nullnull出典表記
imageCreditstring | nullnull写真クレジット
childrenSnippet-内容スロット

スタイル

  • 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型既定値説明
activeStepnumber必須現在のステップ番号
fadeProgressnumber0フェードインのオーバーレイ(1=黒、0=見える)
fadeOutProgressnumber0終端でのフェードアウト

ステップのデータ構造

{
  "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 を効かせるには次が必要。

  1. overflow: hidden ではなく overflow-x: clip を使う:

    :global(html), :global(body) {
      overflow-x: clip;  /* sticky を壊さない */
    }
    
  2. スクロリーのコンテナに isolation を付ける:

    .scroll-section {
      position: relative;
      isolation: isolate;
    }
    
  3. ビジュアル層の構造:

    .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>

トラブルシューティング

動画スクロリーが表示されない

  1. 周囲のセクションの z-index を確認する(z-10 を外す)
  2. ビューポートを覆う position: fixed のオーバーレイがないか確認する
  3. 指定したパスに動画ファイルが実在するか確認する

暗いオーバーレイがページ全体を覆う

原因:フェードのオーバーレイが absolute ではなく fixed になっている。

対処:

.fade-overlay {
  position: absolute;  /* fixed にしない! */
  inset: 0;
}

sticky が効かない

  1. 祖先要素に overflow: hidden がないか確認する(overflow-x: clip を使う)
  2. position: sticky に top: 0 があるか確認する
  3. コンテナに高さ、または十分な内容があるか確認する

ステップが切り替わらない

  1. ステップ要素に十分な高さがあるか確認する(min-height: 70vh など)
  2. IntersectionObserver のマージン設定を確認する
  3. ステップが 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/ を参照。

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

AIが生成しがちな「いかにもAIっぽい」日本語の定型表現・水増し・空疎な形容を取り除き、読者の信頼を損なわない自然な文章に直す。記事・原稿・プレスリリース・告知・SNS投稿など、AIの支援を受けて書いた文章を公開前に点検するときに使う。「AI臭を消したい」「文章が機械っぽい」「人が書いたように直したい」などの依頼で使う。

中文简介正在准备中。当前显示原文说明。

sgbms1/ja-news-skills32026年7月14日 更新

取材・調査・データ報道で使える無料データ源(API)のカタログ。政府統計(e-Stat)、地域経済(RESAS)、気象(気象庁)、法令(e-Gov)、企業開示(EDINET)、書誌・国会会議録(国立国会図書館)、地理空間(国土地理院)、汎用の国際API(GDELT・Open-Meteo・Wayback等)を用途別に整理し、登録要否・利用条件・選び方を示す。「無料のデータ源が欲しい」「このテーマのAPIは?」「公的データをプログラムで取りたい」といった依頼で使う。

中文简介正在准备中。当前显示原文说明。

sgbms1/ja-news-skills32026年7月14日 更新

data-hodo

免费

データ報道(データジャーナリズム)の手順を支援する。データセットの分析、グラフや地図の作成、雑然としたデータのクリーニング、統計の算出、データに基づく記事の構築に使う。「e-Statのデータを分析したい」「人口あたりで比較したい」「グラフが誤解を招かないか確認したい」「統計から記事を組み立てたい」などの依頼で使う。記者・編集者・調査担当向け。

中文简介正在准备中。当前显示原文说明。

sgbms1/ja-news-skills32026年7月14日 更新

取材で集めた資料(開示文書・記事・SNS・反訳など)をデジタルアーカイブ化し、AIで要約・分類・エンティティ抽出・関係づけ(知識グラフ化)して横断検索できる調査基盤を作るための手引き。「取材資料を整理したい」「大量の開示文書をデータベース化したい」「人物・組織・出来事の関係を可視化したい」「調査報道のアーカイブを作りたい」などの依頼で使う。

中文简介正在准备中。当前显示原文说明。

sgbms1/ja-news-skills32026年7月14日 更新

発言・記事・SNS投稿などの主張を体系的にファクトチェックする。主張の抽出、検証可能な命題への分解、根拠の収集、当事者への確認、判定(レーティング)、記録、訂正対応までを支援する。掲載前の裏取り、ファクトチェック記事の作成、編集部の検証手順づくりに使う。「ファクトチェックして」「この発言は本当か」「裏取りの手順を」などの依頼で使う。

中文简介正在准备中。当前显示原文说明。

sgbms1/ja-news-skills32026年7月14日 更新

テキスト・記事・ウェブページの内容を、厳密な検証方法論にもとづいてファクトチェックする。検証可能な主張を抽出し、信頼できる一次情報・公的データ・検証機関と突き合わせ、各主張の真偽と確からしさ(確信度)、情報源の信頼度スコアを評価し、根拠つきの検証レポートにまとめる。「この主張を検証して」「記事をファクトチェックして」「情報源の信頼性を見て」「真偽を判定して」「誤情報か確かめて」などの依頼で使う。付属の references(信頼できる情報源/検証方法論)を参照する検証重視版。

中文简介正在准备中。当前显示原文说明。

sgbms1/ja-news-skills32026年7月14日 更新

查看 sgbms1 的全部技能

报告此技能的问题