本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ボタン

コード欄のコピーボタンで文字が回り、チェックが描かれる

デモ

操作:カーソルを重ねる・クリック。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

用途
ボタン
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

コード欄の右下に、文字とアイコンだけの小さなボタンを置きます。文字は「コピー」「コピー」「コピーしました」の 3 行を縦に積み、升目を 1 行ぶんずつ下げて見せます。カーソルが重なると 1 行、押すと 2 行ぶん下がるので、同じ「コピー」がもう一度回って現れ、次に「コピーしました」に変わります。

アイコンは 2 枚の紙です。カーソルが重なると、奥の紙が左上へ、手前の紙が右下へ 1px ずつ離れます。押すと 2 枚は 0.18 秒で消え、入れ替わりにチェックが描かれます。チェックの線は pathLength を 100 にそろえてあるので、線の実際の長さに関わらず、破線の位置を 104 から 0 へ動かすだけで描き切れます。1 にそろえると値が小さすぎて、描かれる動きが段づきになります。

紙が消えるのを待ってから、0.12 秒後にチェックを描き始めます。同時に始めると、消えかけの紙とチェックが重なって、形が読み取れません。押した後の見た目は 1.6 秒ほど保ってから、ひとりでに戻します。

動きは CSS の切り替えだけで作れます。状態は is-copied を付け外しするだけなので、コピーの成否をそのまま見た目にできます。結果は見た目だけでは伝わらないので、別に置いた role="status" の要素にも文を入れて読み上げさせます。

このデモは、実寸(文字 13px・アイコン 14px)を 2 倍に拡大して見せています。アイコンだけを丸く塗って知らせるものと違い、文字で結果を伝えるので、コードや文章が並ぶ場所になじみます。ドキュメントのコード欄・API キー・招待リンク・クーポンコードのコピーに向いています。

コード

HTML
<figure class="code-block">
  <figcaption class="code-block__lang">HTML</figcaption>
  <pre><code><a class="button" href="/contact/">
  <span class="button__label">お問い合わせ</span>
</a></code></pre>
  <button class="copy" type="button" aria-label="HTML のコードをコピー">
    <!-- 3 行とも読み上げの名前に混ざるので、ラベルは aria-hidden にして aria-label で名前を決める -->
    <span class="copy__label" aria-hidden="true">
      <span class="copy__rows">
        <span>コピー</span>
        <span>コピー</span>
        <span class="copy__done">コピーしました</span>
      </span>
    </span>
    <svg class="copy__icon" viewBox="0 0 14 14" width="14" height="14" aria-hidden="true" focusable="false">
      <path class="copy__sheet copy__back" d="M3 9.5H2A1.5 1.5 0 0 1 .5 8V2A1.5 1.5 0 0 1 2 .5h6A1.5 1.5 0 0 1 9.5 2v1"/>
      <rect class="copy__sheet copy__front" x="4.5" y="4.5" width="9" height="9" rx="1.5"/>
      <!-- pathLength="100" にしておくと、線の実長に関わらず 104 → 0 で描き切れる -->
      <path class="copy__tick" d="M2.5 7.5l3 3 6-7" pathLength="100"/>
    </svg>
  </button>
  <!-- コピーの結果は、見た目だけでは伝わらないので読み上げにも流す -->
  <span class="copy__status" role="status" aria-live="polite"></span>
</figure>
CSS
.code-block {
  --ink: #eeece6; /* コードの文字と、押した後のボタンの色 */
  --dim: rgba(238, 236, 230, 0.45); /* ふだんのボタンの色 */
  position: relative; /* コピーのボタンは、この箱の右下に置く */
  margin: 0;
  border-radius: 6px;
  background: #262626;
  overflow: hidden;
}

.code-block__lang { position: absolute; right: 20px; top: 14px; color: var(--dim); font-size: 12px; letter-spacing: 0.14em; }

/* 下の余白を広めに取り、その中にボタンを置く(コードにかぶらない) */
.code-block pre { margin: 0; padding: 36px 24px 40px; overflow-x: auto; color: var(--ink); font: 14px/1.9 Inconsolata, ui-monospace, monospace; }

.copy {
  position: absolute;
  right: 20px;
  bottom: 14px;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 20px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 13px; /* font: inherit の後に書く(先だと打ち消される) */
  line-height: 18px;
  color: var(--dim);
  cursor: pointer;
  transition: color 0.2s linear;
}

.copy:hover,
.copy.is-copied { color: var(--ink); }

/* 色だけだと手がかりが弱いので、キーボードのときは枠も出す */
.copy:focus-visible {
  color: var(--ink);
  outline: 2px solid currentColor;
  outline-offset: 4px;
}

/* 文字は 3 行を縦に積み、升目をずらして見せる(ふだん → ホバー → コピー後) */
.copy__label { display: block; height: 18px; overflow: hidden; }
.copy__rows > span { height: 18px; white-space: nowrap; }

.copy__rows {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  transition: translate 0.42s cubic-bezier(0.22, 1, 0.36, 1);
}

.copy:hover .copy__rows,
.copy:focus-visible .copy__rows { translate: 0 -18px; }

.copy.is-copied .copy__rows { translate: 0 -36px; }

/* overflow: visible にしないと、離れた紙の角が切れる */
.copy__icon { flex: none; overflow: visible; fill: none; stroke: currentColor; stroke-width: 1; }

.copy__sheet { transition: translate 0.42s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.18s linear; }

.copy:hover .copy__back,
.copy:focus-visible .copy__back { translate: -1px -1px; }

.copy:hover .copy__front,
.copy:focus-visible .copy__front { translate: 1px 1px; }

.copy.is-copied .copy__sheet { opacity: 0; translate: 0 0; }

/* 実線 102・空き 200。pathLength="100" で長さをそろえてあるので、104 → 0 で描き切れる */
.copy__tick {
  stroke-dasharray: 102 200;
  stroke-dashoffset: 104;
  transition: stroke-dashoffset 0.24s cubic-bezier(0.4, 0, 0.2, 1);
}

/* 紙が消えきってから描き始める(0.12 秒待つ) */
.copy.is-copied .copy__tick {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 0.4s cubic-bezier(0.4, 0, 0.2, 1) 0.12s;
}

.copy__status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }

@media (prefers-reduced-motion: reduce) {
  .copy,
  .copy__rows,
  .copy__sheet,
  .copy__tick { transition: none; }
}
JavaScript
const COPIED_MS = 1600; // 「コピーしました」を見せておく時間

// https でない所(社内の検証環境など)では navigator.clipboard が無い。その場合の控え
function legacyCopy(text) {
  const area = document.createElement("textarea");
  area.value = text;
  area.setAttribute("readonly", "");
  area.style.cssText = "position:fixed;top:0;left:0;opacity:0;pointer-events:none";
  document.body.append(area);
  area.select();
  area.setSelectionRange(0, text.length); // iOS は select() だけでは選ばれない
  let ok = false;
  try {
    ok = document.execCommand("copy");
  } catch {
    ok = false;
  }
  area.remove();
  return ok;
}

async function copyText(text) {
  if (!navigator.clipboard) return legacyCopy(text);
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch {
    return legacyCopy(text); // 許可が下りなかったときも控えを試す
  }
}

document.querySelectorAll(".code-block").forEach((block) => {
  const button = block.querySelector(".copy");
  const code = block.querySelector("code");
  const status = block.querySelector(".copy__status");
  const done = button.querySelector(".copy__done");
  let timer = 0;

  button.addEventListener("click", async () => {
    // 行番号や言語の札を擬似要素で出しておけば、textContent はコードだけになる
    const ok = await copyText(code.textContent);
    done.textContent = ok ? "コピーしました" : "コピーできません";
    status.textContent = ok ? "コードをコピーしました" : "コピーできませんでした";
    button.classList.add("is-copied");
    clearTimeout(timer);
    timer = setTimeout(() => {
      button.classList.remove("is-copied");
      status.textContent = "";
    }, COPIED_MS);
  });
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。