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

ボタン

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

デモ

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

仕様

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

しくみと調整

押した瞬間に紙のアイコンが消え、入れ替わりにチェックの線が描かれて、文字が「コピーしました」に変わります。コードを表示する部分の右下に置く、文字とアイコンだけの小さなボタンです。丸く塗ったアイコンだけで結果を知らせる形と違い、文字でも伝えるので、コードや文章が並ぶ場所で浮きません。

文字は「コピー」「コピー」「コピーしました」を縦に積み、1行分だけを見せています。ホバーで1行、押すと2行上へずらすので、カーソルを乗せたときは同じ「コピー」がもう一度回って現れ、押したときにだけ文言が変わります。アイコンは紙を重ねた形で、ホバー中は奥の紙が左上へ、手前の紙が右下へわずかに離れます。

チェックは、紙が消えるのを待ってから描き始めるようにしました。同時に動かすと、消えかけの紙とチェックが重なり、何の形なのか読み取れません。線には pathLength を100と指定してあるので、実際の長さを測らなくても、破線の開始位置を104から0へ動かすだけで描き切れます。押したあとの表示は1.6秒保ってから、自動で元に戻ります。

動きはすべて CSS の切り替えで、状態は is-copied クラスの付け外しだけです。結果は見た目とは別に role="status" の要素へ文として入れ、スクリーンリーダーで読み上げられるようにしています。デモは実寸の2倍に拡大して表示しています。

コード

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 行を縦に積み、1 行ずつずらして見せる(ふだん → ホバー → コピー後) */
.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 サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。