WordPressでも使えるタブで切り替え可能なコードブロックの作成方法

タブで切り替え可能なコードブロックとは?

答え: これのこと👇️

最近、iwb.jp (WordPress) でnpmやpnpmのコマンドを記載することがあるのですが、このようなコードブロックがないと複数行に分けて記載しなければならず、コード部分が見づらくなってしまうのでタブで切り替え可能なコードブロックを自作しました。

HTML、CSS、JavaScriptだけで作成したシンプルな作りなので、すべての環境で使用可能です。

タブで切り替え可能なコードブロックの作り方

まず元となるHTMLとCSSを以下のようにコーディングします。

HTML
<div class="iwb-switcher">
  <div class="switcher">
    <input type="radio" name="tab-radio" id="tab-npm" checked>
    <input type="radio" name="tab-radio" id="tab-pnpm">
    <input type="radio" name="tab-radio" id="tab-yarn">
    <input type="radio" name="tab-radio" id="tab-bun">
    <div class="tabs">
      <label for="tab-npm">
        <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#CB3837" d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"/></svg>
        <span class="label-text">npm</span>
      </label>
      <label for="tab-pnpm">
        <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
          <rect x="0" y="0" width="7.5" height="7.5" fill="#F9AD00"/>
          <rect x="8.25" y="0" width="7.498" height="7.5" fill="#F9AD00"/>
          <rect x="16.5" y="0" width="7.5" height="7.5" fill="#F9AD00"/>
          <rect x="16.5" y="8.25" width="7.5" height="7.5" fill="#F9AD00"/>
          <rect x="8.25" y="8.25" width="7.498" height="7.5" fill="#4E4E4E"/>
          <rect x="0" y="16.5" width="7.5" height="7.5" fill="#4E4E4E"/>
          <rect x="8.25" y="16.5" width="7.498" height="7.5" fill="#4E4E4E"/>
          <rect x="16.5" y="16.5" width="7.5" height="7.5" fill="#4E4E4E"/>
        </svg>
        <span class="label-text">pnpm</span>
      </label>
      <label for="tab-yarn">
        <svg viewBox="0 0 288.6 310.4" xmlns="http://www.w3.org/2000/svg" style="width:16.7px;height:18px"><path fill="#2C8EBB" d="M279.7 248.5c-16 3.8-24.1 7.3-43.9 20.2-31 20-64.8 29.3-64.8 29.3s-2.8 4.2-10.9 6.1c-14 3.4-66.7 6.3-71.5 6.4-12.9.1-20.8-3.3-23-8.6-6.7-16 9.6-23 9.6-23s-3.6-2.2-5.7-4.2c-1.9-1.9-3.9-5.7-4.5-4.3-2.5 6.1-3.8 21-10.5 27.7-9.2 9.3-26.6 6.2-36.9.8-11.3-6 .8-20.1.8-20.1s-6.1 3.6-11-3.8C3 268.2-1.1 256.6 0 242.3 1.4 225.8 19.6 210 19.6 210s-3.2-24.1 7.3-48.8c9.5-22.5 35.1-40.6 35.1-40.6S40.5 96.9 48.5 75.4c5.2-14 7.3-13.9 9-14.5 6-2.3 11.8-4.8 16.1-9.5 21.5-23.2 48.9-18.8 48.9-18.8S135.3-6.7 147.3 1c3.7 2.4 17 32 17 32s14.2-8.3 15.8-5.2c8.6 16.7 9.6 48.6 5.8 68-6.4 32-22.4 49.2-28.8 60-1.5 2.5 17.2 10.4 29 43.1 10.9 29.9 1.2 55 2.9 57.8.3.5.4.7.4.7s12.5 1 37.6-14.5c13.4-8.3 29.3-17.6 47.4-17.8 17.5-.3 18.4 20.3 5.3 23.4z"/></svg>
        <span class="label-text">yarn</span>
      </label>
      <label for="tab-bun">
        <svg viewBox="0 0 80 70" xmlns="http://www.w3.org/2000/svg" style="width:20px;height:17.5px"><title>Bun Logo</title><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>
        <span class="label-text">bun</span>
      </label>
    </div>
    <div class="panels">
      <div class="panel npm">
        <div class="panel-inner">
          <div class="cmd"><span class="cmd-text"></span></div>
          <button class="copy-btn" aria-label="コマンドをコピー">
            <span class="toast">コピーしました。</span>
            <svg class="icon-clipboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="4" width="8" height="4" rx="1"></rect><path d="M8 6H6a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-2"></path></svg>
            <svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"></path></svg>
          </button>
        </div>
      </div>
      <div class="panel pnpm">
        <div class="panel-inner">
          <div class="cmd"><span class="cmd-text"></span></div>
          <button class="copy-btn" aria-label="コマンドをコピー">
            <span class="toast">コピーしました。</span>
            <svg class="icon-clipboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="4" width="8" height="4" rx="1"></rect><path d="M8 6H6a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-2"></path></svg>
            <svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"></path></svg>
          </button>
        </div>
      </div>
      <div class="panel yarn">
        <div class="panel-inner">
          <div class="cmd"><span class="cmd-text"></span></div>
          <button class="copy-btn" aria-label="コマンドをコピー">
            <span class="toast">コピーしました。</span>
            <svg class="icon-clipboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="4" width="8" height="4" rx="1"></rect><path d="M8 6H6a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-2"></path></svg>
            <svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"></path></svg>
          </button>
        </div>
      </div>
      <div class="panel bun">
        <div class="panel-inner">
          <div class="cmd"><span class="cmd-text"></span></div>
          <button class="copy-btn" aria-label="コマンドをコピー">
            <span class="toast">コピーしました。</span>
            <svg class="icon-clipboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="4" width="8" height="4" rx="1"></rect><path d="M8 6H6a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-2"></path></svg>
            <svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"></path></svg>
          </button>
        </div>
      </div>
    </div>
  </div>
</div>
CSS
@scope (.iwb-switcher) {
  :scope {
    --bg: #f6f5f3;
    --panel: #fbfaf8;
    --ink: #1c1b19;
    --gray: #8a8579;
    --line: #e4e1da;
    --accent: #2f6f4f;
    --mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;

    width: 100%;
    max-width: 560px;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: 14px;
    overflow: hidden;
    box-shadow: 0 1px 2px rgba(28,27,25,0.04);
    font-family: -apple-system, BlinkMacSystemFont, 'Hiragino Sans', 'Noto Sans JP', sans-serif;
  }

  * { box-sizing: border-box; }

  .cmd {
    background-color: initial;
  }

  .switcher { position: relative; }

  .switcher input[type="radio"] {
    position: absolute;
    opacity: 0;
    pointer-events: none;
  }

  .tabs {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 10px 14px 0;
    border-bottom: 1px solid var(--line);
  }

  .tabs label {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px;
    font-size: 14.5px;
    color: var(--gray);
    cursor: pointer;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    user-select: none;
    transition: color 0.15s ease;
    white-space: nowrap;
  }

  .tabs label:is(:hover, :focus) { color: var(--ink); }
  .tabs label svg { width: 18px; height: 18px; flex-shrink: 0; display: block; }
  .panels { position: relative; }
  .panel { display: none; padding: 20px 18px 22px; }
  .panel-inner {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    background: #f1efe9;
    border: 1px solid var(--line);
    border-radius: 10px;
    padding: 12px 8px 12px 16px;
  }

  .cmd {
    font-family: var(--mono);
    font-size: 14.5px;
    line-height: 1.5;
    color: var(--ink);
    overflow-x: auto;
    white-space: pre;
  }

  .copy-btn {
    position: relative;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
    border: none;
    background: transparent;
    border-radius: 8px;
    cursor: pointer;
    color: var(--gray);
    transition: background 0.15s ease, color 0.15s ease;
  }
  .copy-btn:focus { background: none; }
  .copy-btn:hover { background: #ebe4d2; }
  .copy-btn svg { width: 17px; height: 17px; pointer-events: none; }
  .copy-btn .icon-check { display: none; color: var(--accent); }
  .copy-btn.copied .icon-clipboard { display: none; }
  .copy-btn.copied .icon-check { display: block; }

  .toast {
    position: absolute;
    bottom: calc(100% + 8px);
    right: 0;
    background: var(--ink);
    color: #fff;
    font-size: 12px;
    line-height: 1;
    padding: 6px 10px;
    border-radius: 6px;
    white-space: nowrap;
    opacity: 0;
    transform: translateY(4px);
    transition: opacity 0.15s ease, transform 0.15s ease;
    pointer-events: none;
  }

  .toast::after {
    content: '';
    position: absolute;
    top: 100%;
    right: 12px;
    border: 5px solid transparent;
    border-top-color: var(--ink);
  }

  .toast.show {
    opacity: 1;
    transform: translateY(0);
  }

  #tab-npm:checked  ~ .tabs label[for="tab-npm"],
  #tab-yarn:checked ~ .tabs label[for="tab-yarn"],
  #tab-pnpm:checked ~ .tabs label[for="tab-pnpm"],
  #tab-bun:checked  ~ .tabs label[for="tab-bun"] {
    color: var(--ink);
    border-bottom-color: var(--accent);
  }

  #tab-npm:checked  ~ .panels .panel.npm,
  #tab-yarn:checked ~ .panels .panel.yarn,
  #tab-pnpm:checked ~ .panels .panel.pnpm,
  #tab-bun:checked  ~ .panels .panel.bun {
    display: block;
  }

  @media (max-width: 480px) {
    .tabs { padding: 8px 8px 0; }
    .tabs label { padding: 10px 8px; font-size: 13px; }
    .tabs label span.label-text { display: none; }
  }
}

CSSがほかのWebサイト内の既存のコードに影響を与えないように@scopeを使って適用範囲をスコープしています。

JavaScriptはコードの挿入とクリップボードへのコピーに使用しています。

JavaScript
document.addEventListener('DOMContentLoaded', () => {
  const commands = [
    `npx vitest init browser`,
    `pnpm dlx vitest init browser`,
    `yarn dlx vitest init browser`,
    `bunx vitest init browser`
  ]

  const fallbackCopy = (text) => {
    const textarea = document.createElement('textarea')
    textarea.value = text
    textarea.style.position = 'fixed'
    textarea.style.top = '-1000px'
    textarea.style.left = '-1000px'
    document.body.appendChild(textarea)
    textarea.focus()
    textarea.select()
    let ok = false
    try {
      ok = document.execCommand('copy')
    } catch {
      ok = false
    }
    document.body.removeChild(textarea)
    return ok
  }

  const copyText = async (text) => {
    if (navigator.clipboard && window.isSecureContext) {
      try {
        await navigator.clipboard.writeText(text)
        return true
      } catch {
        return fallbackCopy(text)
      }
    }
    return fallbackCopy(text)
  }

  const panels = document.querySelectorAll('.iwb-switcher .panel')
  panels.forEach((panel, index) => {
    const cmdText = commands[index]
    if (!cmdText) return

    const cmdTextEl = panel.querySelector('.cmd-text')
    const btn = panel.querySelector('.copy-btn')

    if (cmdTextEl) {
      cmdTextEl.textContent = cmdText
    }
    if (btn) {
      btn.setAttribute('data-cmd', cmdText)
      btn.addEventListener('click', async () => {
        const text = btn.getAttribute('data-cmd')
        const toast = btn.querySelector('.toast')
        const success = await copyText(text)

        if (success) {
          btn.classList.add('copied')
          toast?.classList.add('show')
          setTimeout(() => {
            btn.classList.remove('copied')
            toast?.classList.remove('show')
          }, 1500)
        }
      })
    }
  })
})

WordPressの場合は以上のHTML、CSS、JavaScriptを「カスタムHTML」で挿入するだけで実装できます。

表示するコードは配列にテンプレートリテラルを入れているので、${foo} で変数を入れたり、改行コードの挿入による改行も容易にできます。

JavaScript
const commands = [
  `npx vitest init browser`,
  `pnpm dlx vitest init browser`,
  `yarn dlx vitest init browser`,
  `bunx vitest init browser`
]

※ 画面内に複数のコードブロックを配置する場合は、別の名前のものを作成するかforEachを使うなどの対応が必要

まとめ

記事のタブ切り替えコードブロックはWordPressプラグインやライブラリなどを使用していないので、基本的にアップデートの必要がなく、自由にカスタマイズできます。

HTML、CSS、JavaScriptの知識があれば自由な形で作れるので、タブ切り替えのコードブロックはこちらのコードを参考にして作成することをおすすめします。