パスドローイング(手書きアニメ)

stroke-dasharrayとdashoffsetでSVGの線が描かれていく演出。グラフやロゴ、署名の登場アニメに使えます。

#svg#css#animation#stroke

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura:メンバー直筆サインが描かれるグッズ紹介 -->
<section class="sk-sign">
  <div class="sk-sign__photo" aria-hidden="true"></div>

  <div class="sk-sign__panel">
    <span class="sk-sign__tag">GOODS / 限定</span>
    <h2 class="sk-sign__title">直筆サイン入り<br>チェキ風ブロマイド</h2>

    <!-- 主役:手書き風に描かれるサインSVG -->
    <svg class="sk-draw" viewBox="0 0 280 140" role="img" aria-label="手書き風に描かれるメンバーのサイン">
      <path class="sk-name" d="M20 95 q12 -55 26 -18 t26 -14 q14 38 30 4 t26 -26" />
      <path class="sk-name2" d="M120 100 q18 -48 34 -8 t34 -22 q12 30 30 2" />
      <path class="sk-heart" d="M210 70 c-9 -12 -26 -2 -16 12 c4 6 16 14 16 14 c0 0 12 -8 16 -14 c10 -14 -7 -24 -16 -12 Z" />
      <path class="sk-under" d="M20 122 H250" />
    </svg>

    <button class="sk-sign__btn" type="button">▶ サインを再生</button>
    <p class="sk-sign__note">※ 全7種ランダム封入。¥800(税込)</p>
  </div>
</section>
CSS
/* Sakura:直筆サイン入りグッズ紹介 */
:root {
  --pink: #ffd1e0;
  --pink-deep: #ff7aa8;
  --gray: #f3f4f7;
  --ink: #4a4453;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, #fff 0%, var(--pink) 100%);
  font-family: "Hiragino Kaku Gothic ProN", "Yu Gothic", system-ui, sans-serif;
  color: var(--ink);
  overflow: hidden;
}

.sk-sign {
  display: grid;
  grid-template-columns: 150px 1fr;
  width: min(620px, 94vw);
  background: #fff;
  border-radius: 22px;
  overflow: hidden;
  box-shadow: 0 22px 50px -22px rgba(255, 122, 168, 0.55);
}

/* メンバー写真 */
.sk-sign__photo {
  background:
    linear-gradient(180deg, rgba(255,122,168,0.12), rgba(255,209,224,0.35)),
    url("https://picsum.photos/300/500?random=61") center/cover no-repeat;
}

.sk-sign__panel { padding: 22px 26px 20px; }
.sk-sign__tag {
  display: inline-block;
  font-size: 10px;
  letter-spacing: 0.18em;
  font-weight: 700;
  color: var(--pink-deep);
  background: var(--pink);
  padding: 4px 10px;
  border-radius: 999px;
}
.sk-sign__title {
  margin: 12px 0 6px;
  font-size: 20px;
  line-height: 1.45;
  font-weight: 800;
}

/* 主役のサインSVG:線は最初隠し、--len分のdashを流す */
.sk-draw { width: 100%; height: auto; display: block; margin: 6px 0 4px; }
.sk-draw path {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.sk-name {
  stroke: var(--pink-deep);
  stroke-width: 3.5;
  stroke-dasharray: var(--len, 400);
  stroke-dashoffset: var(--len, 400);
  animation: skDash 1.6s ease forwards;
}
.sk-name2 {
  stroke: var(--pink-deep);
  stroke-width: 3.5;
  stroke-dasharray: var(--len, 400);
  stroke-dashoffset: var(--len, 400);
  animation: skDash 1.4s ease 0.6s forwards;
}
.sk-heart {
  stroke: #ff4f8b;
  stroke-width: 3;
  fill: rgba(255, 79, 139, 0);
  stroke-dasharray: var(--len, 200);
  stroke-dashoffset: var(--len, 200);
  animation: skDash 0.9s ease 1.4s forwards, skFill 0.4s ease 2.2s forwards;
}
.sk-under {
  stroke: rgba(74, 68, 83, 0.18);
  stroke-width: 1.5;
  stroke-dasharray: var(--len, 240);
  stroke-dashoffset: var(--len, 240);
  animation: skDash 0.8s ease 0.2s forwards;
}

@keyframes skDash { to { stroke-dashoffset: 0; } }
@keyframes skFill { to { fill: rgba(255, 79, 139, 0.9); } }

.sk-sign__btn {
  font: 700 12px/1 "Hiragino Kaku Gothic ProN", sans-serif;
  color: var(--pink-deep);
  background: var(--gray);
  border: 1px solid rgba(255, 122, 168, 0.4);
  padding: 8px 16px;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.2s, transform 0.1s;
}
.sk-sign__btn:hover { background: var(--pink); }
.sk-sign__btn:active { transform: scale(0.96); }
.sk-sign__note { margin: 12px 0 0; font-size: 11px; color: #9b94a5; }

@media (prefers-reduced-motion: reduce) {
  .sk-draw path { animation: none; stroke-dashoffset: 0; }
  .sk-heart { fill: rgba(255, 79, 139, 0.9); }
}
JavaScript
// 各パスの実長を測って dasharray/offset に反映(サインがきれいに描かれる)
const draw = document.querySelector(".sk-draw");

if (draw) {
  const paths = draw.querySelectorAll("path");

  // パス長を CSS 変数 --len へ渡す
  const measure = (el) => {
    if (typeof el.getTotalLength === "function") {
      el.style.setProperty("--len", Math.ceil(el.getTotalLength()));
    }
  };
  paths.forEach(measure);

  // 再生ボタン:アニメをリスタート
  const btn = document.querySelector(".sk-sign__btn");
  const replay = () => {
    paths.forEach((el) => {
      el.style.animation = "none";
      void el.offsetWidth; // リフローでリセット
      el.style.animation = "";
    });
  };
  btn?.addEventListener("click", replay);
}

コード

HTML
<!-- パスドローイング: stroke-dasharray/offset で線が描かれるアニメ -->
<div class="stage">
  <svg class="draw" viewBox="0 0 320 200" role="img" aria-label="手書き風に描かれるグラフと署名">
    <!-- 折れ線グラフ -->
    <polyline class="line" points="20,160 80,110 130,135 185,60 250,90 300,40" />
    <!-- 下線 -->
    <path class="underline" d="M20 178 H300" />
    <!-- 署名風の筆記体パス -->
    <path class="sign" d="M40 150 q15 -50 30 -10 t30 -10 q20 40 40 0 t40 -20" />
    <!-- 円(ハイライト) -->
    <circle class="dot" cx="185" cy="60" r="7" />
  </svg>
  <button class="replay" type="button" aria-label="もう一度再生">▶ 再描画</button>
</div>
CSS
:root {
  --ink: #f4f7ff;
  --accent: #5eead4;
  --accent2: #818cf8;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  /* 斜めグラデの落ち着いた背景 */
  background:
    radial-gradient(120% 120% at 80% 0%, #1e293b 0%, #0f172a 55%, #020617 100%);
}
.stage {
  position: relative;
  width: min(92%, 460px);
  padding: 22px 26px 30px;
  border-radius: 18px;
  background: rgba(255, 255, 255, .03);
  border: 1px solid rgba(255, 255, 255, .08);
  box-shadow: 0 30px 60px -25px rgba(0, 0, 0, .8);
}
.draw { width: 100%; height: auto; display: block; }

/* 共通: 線は最初フルに隠し、--len分のdashを流す */
.draw [class] {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.line {
  stroke: var(--accent);
  stroke-width: 3.5;
  stroke-dasharray: var(--len, 400);
  stroke-dashoffset: var(--len, 400);
  animation: dash 1.6s ease forwards;
}
.underline {
  stroke: rgba(255, 255, 255, .25);
  stroke-width: 1.5;
  stroke-dasharray: var(--len, 300);
  stroke-dashoffset: var(--len, 300);
  animation: dash 1s ease .2s forwards;
}
.sign {
  stroke: var(--accent2);
  stroke-width: 2.5;
  stroke-dasharray: var(--len, 400);
  stroke-dashoffset: var(--len, 400);
  animation: dash 1.8s ease .6s forwards;
}
.dot {
  fill: var(--accent);
  stroke: #0f172a;
  stroke-width: 2;
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(0);
  animation: pop .4s cubic-bezier(.34, 1.56, .64, 1) 1.4s forwards;
}

@keyframes dash { to { stroke-dashoffset: 0; } }
@keyframes pop { to { transform: scale(1); } }

.replay {
  position: absolute;
  right: 14px;
  bottom: 10px;
  font: 600 12px/1 "Segoe UI", sans-serif;
  color: var(--ink);
  background: rgba(129, 140, 248, .18);
  border: 1px solid rgba(129, 140, 248, .5);
  padding: 6px 12px;
  border-radius: 999px;
  cursor: pointer;
  transition: background .2s, transform .1s;
}
.replay:hover { background: rgba(129, 140, 248, .35); }
.replay:active { transform: scale(.96); }

@media (prefers-reduced-motion: reduce) {
  .line, .underline, .sign { animation: none; stroke-dashoffset: 0; }
  .dot { animation: none; transform: scale(1); }
}
JavaScript
// 各パスの実長を取得して dasharray/offset に反映(線がきれいに描かれる)
const svg = document.querySelector(".draw");
if (svg) {
  const shapes = svg.querySelectorAll(".line, .underline, .sign");

  // パスの長さを測って CSS 変数に渡す
  const measure = (el) => {
    if (typeof el.getTotalLength === "function") {
      const len = Math.ceil(el.getTotalLength());
      el.style.setProperty("--len", len);
    }
  };
  shapes.forEach(measure);

  // 再描画ボタン: アニメをリスタート
  const replay = document.querySelector(".replay");
  const restart = () => {
    svg.querySelectorAll(".line, .underline, .sign, .dot").forEach((el) => {
      el.style.animation = "none";
      void el.offsetWidth; // リフローでアニメをリセット
      el.style.animation = "";
    });
  };
  replay?.addEventListener("click", restart);
}

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「パスドローイング(手書きアニメ)」の効果を追加してください。

# 追加してほしい効果
パスドローイング(手書きアニメ)(SVG エフェクト)
stroke-dasharrayとdashoffsetでSVGの線が描かれていく演出。グラフやロゴ、署名の登場アニメに使えます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- パスドローイング: stroke-dasharray/offset で線が描かれるアニメ -->
<div class="stage">
  <svg class="draw" viewBox="0 0 320 200" role="img" aria-label="手書き風に描かれるグラフと署名">
    <!-- 折れ線グラフ -->
    <polyline class="line" points="20,160 80,110 130,135 185,60 250,90 300,40" />
    <!-- 下線 -->
    <path class="underline" d="M20 178 H300" />
    <!-- 署名風の筆記体パス -->
    <path class="sign" d="M40 150 q15 -50 30 -10 t30 -10 q20 40 40 0 t40 -20" />
    <!-- 円(ハイライト) -->
    <circle class="dot" cx="185" cy="60" r="7" />
  </svg>
  <button class="replay" type="button" aria-label="もう一度再生">▶ 再描画</button>
</div>

【CSS】
:root {
  --ink: #f4f7ff;
  --accent: #5eead4;
  --accent2: #818cf8;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  /* 斜めグラデの落ち着いた背景 */
  background:
    radial-gradient(120% 120% at 80% 0%, #1e293b 0%, #0f172a 55%, #020617 100%);
}
.stage {
  position: relative;
  width: min(92%, 460px);
  padding: 22px 26px 30px;
  border-radius: 18px;
  background: rgba(255, 255, 255, .03);
  border: 1px solid rgba(255, 255, 255, .08);
  box-shadow: 0 30px 60px -25px rgba(0, 0, 0, .8);
}
.draw { width: 100%; height: auto; display: block; }

/* 共通: 線は最初フルに隠し、--len分のdashを流す */
.draw [class] {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.line {
  stroke: var(--accent);
  stroke-width: 3.5;
  stroke-dasharray: var(--len, 400);
  stroke-dashoffset: var(--len, 400);
  animation: dash 1.6s ease forwards;
}
.underline {
  stroke: rgba(255, 255, 255, .25);
  stroke-width: 1.5;
  stroke-dasharray: var(--len, 300);
  stroke-dashoffset: var(--len, 300);
  animation: dash 1s ease .2s forwards;
}
.sign {
  stroke: var(--accent2);
  stroke-width: 2.5;
  stroke-dasharray: var(--len, 400);
  stroke-dashoffset: var(--len, 400);
  animation: dash 1.8s ease .6s forwards;
}
.dot {
  fill: var(--accent);
  stroke: #0f172a;
  stroke-width: 2;
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(0);
  animation: pop .4s cubic-bezier(.34, 1.56, .64, 1) 1.4s forwards;
}

@keyframes dash { to { stroke-dashoffset: 0; } }
@keyframes pop { to { transform: scale(1); } }

.replay {
  position: absolute;
  right: 14px;
  bottom: 10px;
  font: 600 12px/1 "Segoe UI", sans-serif;
  color: var(--ink);
  background: rgba(129, 140, 248, .18);
  border: 1px solid rgba(129, 140, 248, .5);
  padding: 6px 12px;
  border-radius: 999px;
  cursor: pointer;
  transition: background .2s, transform .1s;
}
.replay:hover { background: rgba(129, 140, 248, .35); }
.replay:active { transform: scale(.96); }

@media (prefers-reduced-motion: reduce) {
  .line, .underline, .sign { animation: none; stroke-dashoffset: 0; }
  .dot { animation: none; transform: scale(1); }
}

【JavaScript】
// 各パスの実長を取得して dasharray/offset に反映(線がきれいに描かれる)
const svg = document.querySelector(".draw");
if (svg) {
  const shapes = svg.querySelectorAll(".line, .underline, .sign");

  // パスの長さを測って CSS 変数に渡す
  const measure = (el) => {
    if (typeof el.getTotalLength === "function") {
      const len = Math.ceil(el.getTotalLength());
      el.style.setProperty("--len", len);
    }
  };
  shapes.forEach(measure);

  // 再描画ボタン: アニメをリスタート
  const replay = document.querySelector(".replay");
  const restart = () => {
    svg.querySelectorAll(".line, .underline, .sign, .dot").forEach((el) => {
      el.style.animation = "none";
      void el.offsetWidth; // リフローでアニメをリセット
      el.style.animation = "";
    });
  };
  replay?.addEventListener("click", restart);
}

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。