CSS

CSSでラジオボタンをカスタマイズする方法|3パターンのデザインを徹底解説

みなと

Webサイト制作の現場では、デザインカンプ通りのラジオボタンを実装しなければならない場面が頻繁にあります。しかし、デフォルトのラジオボタンはブラウザが独自のスタイルを持っているため、CSSで見た目を変えようとしてもうまくいかないことがほとんどです。

UI実装担当
UI実装担当

どうすればオリジナルのデザインに変えられるの?

と困った経験はありませんか?

この記事では、CSSでラジオボタンを自由にカスタマイズする方法を、3つのデザインパターンで解説します。シンプルな丸型からカード型、チェックマーク付きカードまで、レスポンシブにも対応したコードとともに紹介します。

デフォルトのラジオボタンはCSSで変えにくい

デフォルトのラジオボタンは、ブラウザが独自のスタイルを持っているため、CSSで見た目を変えようとしても思い通りにならないことがほとんどです。appearance プロパティでスタイルをリセットする方法もありますが、ブラウザによって挙動が異なるため完全にコントロールするのが難しいのが現状です。

なお、現在の主要ブラウザではデフォルトの見た目の差は以前ほど大きくありませんが、環境によって微妙な違いが生じることがあります。

PC(Windows)のEdge・Chrome・Firefox、PC(macOS)のSafari・Chrome・Firefox、タブレット(iPadOS)のSafari、スマホ(iOS)のSafari、スマホ(Android)のChromeにおけるデフォルトのラジオボタン表示を並べた比較画像
同じHTMLでも、OS・ブラウザ・端末によって見た目に差が出ることがあります。

そのため、この記事ではデフォルトのラジオボタンを非表示にして独自のUIを組み立てるアプローチを紹介します。これにより、どの環境でも同じデザインに揃えることができます。

CSSでラジオボタンをカスタマイズする基本的な考え方

ラジオボタンをCSSでカスタマイズするには、デフォルトのラジオボタンを見えない状態にして、独自のUIを別途HTMLで組み立てるアプローチを取ります。

具体的には、input[type="radio"] を画面上から隠しつつ、label 要素の中に独自デザインの要素(span など)を用意して、そちらにスタイルを当てます。input の状態(選択済み・disabled など)はCSSのセレクターで参照できるため、JavaScriptなしで動的なスタイル切り替えが実現できます。

非表示にしたinput要素と、デザインを担うspan要素の関係を示した図。input要素は「非表示」、span要素は「自由にデザイン」とラベリングされており、隣接セレクタで連携する仕組みを図解している。

visually-hiddenとは

input を非表示にする方法として display: none が思い浮かぶかもしれませんが、この記事では visually-hidden という手法を使います。

display: none はスクリーンリーダー(読み上げソフト)にも認識されなくなるため、視覚に頼らないユーザーがラジオボタンを操作できなくなってしまいます。visually-hiddenは、見た目上は非表示にしつつ、スクリーンリーダーには認識させることができる手法です。

input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

要素を1px×1pxまで縮小し、clip ではみ出た部分を切り取ることで、画面上には見えない状態にしています。フォームのアクセシビリティを保つために、display: none よりもこの方法を推奨します。

パターン1 — シンプル丸型

まず最もベーシックな、シンプルな丸型のラジオボタンです。デフォルトに近いUI構造を保ちながら、見た目を整えたいときに使いやすいパターンです。

完成デモの紹介

完成したデモです。実際に操作して動作を確認してみてください。

コード全体(HTML / CSS)

HTMLコード

<ul class="radio-simple-list">
  <li>
    <label>
      <input type="radio" name="plan-simple" value="a">
      <span class="dot"></span>
      <span class="text">選択肢 A</span>
    </label>
  </li>
  <li>
    <label>
      <input type="radio" name="plan-simple" value="b">
      <span class="dot"></span>
      <span class="text">選択肢 B</span>
    </label>
  </li>
  <li>
    <label>
      <input type="radio" name="plan-simple" value="c" disabled>
      <span class="dot"></span>
      <span class="text">選択肢 C(disabled)</span>
    </label>
  </li>
</ul>

CSSコード

.radio-simple-list {
  list-style: none;
  display: flex;
  margin: 0;
  padding: 0;
}

@media (min-width: 768px) {
  .radio-simple-list {
    justify-content: center;
    gap: 40px;
  }
}

@media (max-width: 767px) {
  .radio-simple-list {
    flex-direction: column;
    gap: 15px;
  }
}

.radio-simple-list label {
  display: block;
  position: relative;
  padding-left: 38px;
  font-size: 18px;
  line-height: 1.5;
  cursor: pointer;
}

.radio-simple-list label:has(input:disabled) {
  cursor: not-allowed;
}

.radio-simple-list input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

.radio-simple-list .dot {
  position: absolute;
  left: 0;
  top: 0;
  width: 28px;
  height: 28px;
  box-sizing: border-box;
  border: 1px solid #058ad5;
  border-radius: 50%;
  background: #fff;
  transition: box-shadow 300ms ease;
}

.radio-simple-list .dot::before {
  content: "";
  display: block;
  position: absolute;
  left: 50%;
  top: 50%;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #dde3e8;
  transform: translate(-50%, -50%);
}

.radio-simple-list label:not(:has(input:disabled)):hover .dot,
.radio-simple-list input:focus-visible + .dot {
  box-shadow: 0 0 15px rgba(5, 138, 213, 0.4);
}

.radio-simple-list input:checked + .dot {
  background: #058ad5;
}

.radio-simple-list input:checked + .dot::before {
  background: #fff;
}

.radio-simple-list input:disabled + .dot {
  border-color: #999;
  opacity: 0.3;
}

.radio-simple-list input:disabled ~ .text {
  opacity: 0.3;
}

コードのポイント解説

パターン1のコードについて、特に重要なポイントを5つ解説します。

ポイント1:label 全体をクリック領域にする

label 要素で input.dot.text をまとめて囲むことで、テキスト部分をクリックしてもラジオボタンが選択されるようになります。

また、通常時は cursor: pointer を指定していますが、disabled時はクリックできないため cursor: not-allowed に切り替えます。:has() を使うことで、label の中に input:disabled が存在するときだけスタイルを当てることができます。

.radio-simple-list label {
  cursor: pointer;
}

.radio-simple-list label:has(input:disabled) {
  cursor: not-allowed;
}
スワン
スワン

:has() は比較的新しいCSSですが、主要ブラウザではすでに対応済みです。

ポイント2:ラジオボタンの丸を描く

visually-hiddenで input を非表示にしている代わりに、.dot という span 要素でラジオボタンのインジケーター(丸い部分)を再現しています。

外側の丸(.dot)を border-radius: 50% で円形にし、中央の丸は ::before 疑似要素で表現しています。position: absolutetransform: translate(-50%, -50%) で正確に中央へ配置しています。

.radio-simple-list .dot {
  position: absolute;
  width: 28px;
  height: 28px;
  border: 1px solid #058ad5;
  border-radius: 50%;
}

.radio-simple-list .dot::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #dde3e8;
  transform: translate(-50%, -50%);
}

ポイント3:ホバー・フォーカス時に光彩を表示する

ホバー時とキーボードフォーカス時に、box-shadow で青い光彩を表示してフィードバックを伝えます。

disabled時にもホバーが発火しないよう、:not(:has(input:disabled)) で除外しています。キーボード操作時のフォーカスは :focus-visible で対応しています。

.radio-simple-list label:not(:has(input:disabled)):hover .dot,
.radio-simple-list input:focus-visible + .dot {
  box-shadow: 0 0 15px rgba(5, 138, 213, 0.4);
}
みなと
みなと

:not(:has(input:disabled)) の部分、少し複雑に見えますが、意味は「disabled状態のinputを含まないlabel」です。

ポイント4:選択状態のスタイルを切り替える

input が選択されたとき(:checked)、隣接する .dot にスタイルを当てます。+ は隣接兄弟結合子で、直後の兄弟要素1つだけを対象にします。

選択時は .dot の背景色を青に変え、::before の色を白にすることで、選択済みの見た目を表現しています。

.radio-simple-list input:checked + .dot {
  background: #058ad5;
}

.radio-simple-list input:checked + .dot::before {
  background: #fff;
}

ポイント5:disabled時の見た目を表現する

disabled時は操作できないことをユーザーに伝えるため、.dotopacity を下げて薄く見せ、border-color をグレーに変えています。テキスト(.text)も同様に opacity を下げて、全体的に非活性であることを視覚的に示します。

.radio-simple-list input:disabled + .dot {
  border-color: #999;
  opacity: 0.3;
}

.radio-simple-list input:disabled ~ .text {
  opacity: 0.3;
}

.text には + ではなく ~(一般兄弟結合子)を使っています。input.text の間に .dot が挟まっているため、直後の要素だけを対象にする + では届かないためです。

パターン2 — カード型

次に、選択肢全体がカードになるデザインです。選択肢がボタンのように見えるため、プランや商品の選択など、選択肢を視覚的に強調したい場面に向いています。

完成デモの紹介

完成したデモです。実際に操作して動作を確認してみてください。

コード全体(HTML / CSS)

HTMLコード

<ul class="radio-card-list">
  <li>
    <label>
      <input type="radio" name="size" value="s" checked>
      <span class="card">S:スモール</span>
    </label>
  </li>
  <li>
    <label>
      <input type="radio" name="size" value="m">
      <span class="card">M:ミディアム</span>
    </label>
  </li>
  <li>
    <label>
      <input type="radio" name="size" value="l">
      <span class="card">L:ラージ</span>
    </label>
  </li>
  <li>
    <label>
      <input type="radio" name="size" value="xl" disabled>
      <span class="card">XL:特大(disabled)</span>
    </label>
  </li>
</ul>

CSSコード

.radio-card-list {
  list-style: none;
  display: flex;
  margin: 0;
  padding: 0;
}

@media (min-width: 768px) {
  .radio-card-list {
    justify-content: center;
    gap: 15px;
  }
}

@media (max-width: 767px) {
  .radio-card-list {
    flex-direction: column;
    gap: 10px;
  }
}

.radio-card-list label {
  display: block;
  position: relative;
  font-size: 18px;
  line-height: 1.5;
  cursor: pointer;
}

.radio-card-list label:has(input:disabled) {
  cursor: not-allowed;
}

.radio-card-list input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

.radio-card-list .card {
  display: block;
  position: relative;
  padding: 10px 15px;
  border: 1px solid #ccc;
  border-radius: 6px;
  background: #fff;
  transition: border-color 300ms ease, box-shadow 300ms ease;
}

.radio-card-list label:not(:has(input:disabled)):hover .card,
.radio-card-list input:focus-visible + .card {
  border-color: #058ad5;
  box-shadow: 0 0 15px rgba(5, 138, 213, 0.4);
}

.radio-card-list input:checked + .card {
  border-color: #058ad5;
  background: #e6f1fb;
  color: #058ad5;
  font-weight: 500;
}

.radio-card-list input:disabled + .card {
  opacity: 0.3;
}

コードのポイント解説

クリック領域・ホバー・フォーカス・disabled時の制御については、パターン1と同じアプローチを採用しています。ここではパターン2固有のポイントを解説します。

ポイント1:カードUIを作る

パターン1では .dot という独立したインジケーターを用意しましたが、このパターンでは .card というひとつの span にテキストとボーダーをまとめて持たせます。display: block にすることで label の幅いっぱいに広がり、カード全体がクリック領域になります。

.radio-card-list .card {
  display: block;
  padding: 10px 15px;
  border: 1px solid #ccc;
  border-radius: 6px;
  background: #fff;
}

ポイント2:選択状態のスタイルを切り替える

選択時は input:checked + .cardカード全体の背景色・ボーダー色・文字色をまとめて変えます。パターン1と同様に +(隣接兄弟結合子)を使い、input の直後にある .card を対象にしています。

.radio-card-list input:checked + .card {
  border-color: #058ad5;
  background: #E6F1FB;
  color: #058ad5;
  font-weight: 500;
}

パターン3 — チェックマーク付きカード

最後は、カード内に複数の情報(タイトル・価格など)を持たせ、選択時にチェックマークが表示されるデザインです。料金プランの選択など、情報量が多い選択肢に向いています。

完成デモの紹介

完成したデモです。実際に操作して動作を確認してみてください。

コード全体(HTML / CSS)

HTMLコード

<ul class="radio-check-list">
  <li>
    <label>
      <input type="radio" name="plan-check" value="free" checked>
      <span class="card">
        <span class="text">
          <span class="title">フリー</span>
          <span class="price">¥0 / 月</span>
        </span>
        <span class="check">
          <svg width="10" height="8" viewBox="0 0 10 8" fill="none" aria-hidden="true">
            <path d="M1 4l3 3 5-6" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
          </svg>
        </span>
      </span>
    </label>
  </li>
  <li>
    <label>
      <input type="radio" name="plan-check" value="pro">
      <span class="card">
        <span class="text">
          <span class="title">プロ</span>
          <span class="price">¥1,500 / 月</span>
        </span>
        <span class="check">
          <svg width="10" height="8" viewBox="0 0 10 8" fill="none" aria-hidden="true">
            <path d="M1 4l3 3 5-6" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
          </svg>
        </span>
      </span>
    </label>
  </li>
  <li>
    <label>
      <input type="radio" name="plan-check" value="biz" disabled>
      <span class="card">
        <span class="text">
          <span class="title">ビジネス</span>
          <span class="price">要お問い合わせ</span>
        </span>
        <span class="check">
          <svg width="10" height="8" viewBox="0 0 10 8" fill="none" aria-hidden="true">
            <path d="M1 4l3 3 5-6" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
          </svg>
        </span>
      </span>
    </label>
  </li>
</ul>

CSSコード

.radio-check-list {
  list-style: none;
  display: flex;
  margin: 0;
  padding: 0;
}

@media (min-width: 768px) {
  .radio-check-list {
    justify-content: center;
    gap: 15px;
  }
}

@media (max-width: 767px) {
  .radio-check-list {
    flex-direction: column;
    gap: 10px;
  }
}

.radio-check-list label {
  display: block;
  position: relative;
  font-size: 18px;
  line-height: 1.5;
  cursor: pointer;
}

.radio-check-list label:has(input:disabled) {
  cursor: not-allowed;
}

.radio-check-list input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

.radio-check-list .card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  padding: 10px 15px;
  border: 1px solid #ccc;
  border-radius: 6px;
  background: #fff;
  transition: border-color 300ms ease, box-shadow 300ms ease;
}

@media (min-width: 768px) {
  .radio-check-list .card {
    width: 160px;
  }
}

.radio-check-list .text {
  display: flex;
  flex-direction: column;
}

.radio-check-list .title {
  font-weight: 500;
}

.radio-check-list .price {
  font-size: 13px;
  color: #7a7a7a;
}

.radio-check-list .check {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border: 1px solid #ccc;
  border-radius: 50%;
  background: #fff;
}

.radio-check-list .check svg {
  display: none;
}

.radio-check-list label:not(:has(input:disabled)):hover .card,
.radio-check-list input:focus-visible + .card {
  border-color: #058ad5;
  box-shadow: 0 0 15px rgba(5, 138, 213, 0.4);
}

.radio-check-list input:checked + .card {
  border-color: #058ad5;
  background: #e6f1fb;
}

.radio-check-list input:checked + .card .check {
  background: #058ad5;
  border-color: #058ad5;
}

.radio-check-list input:checked + .card .check svg {
  display: block;
}

.radio-check-list input:disabled + .card {
  opacity: 0.3;
}

コードのポイント解説

クリック領域・ホバー・フォーカス・disabled時の制御については、パターン1と同じアプローチを採用しています。ここではパターン3固有のポイントを解説します。

ポイント1:複数情報を持たせるHTML構造

このパターンはカード内にタイトルと価格の2つの情報を持たせるため、.text でテキスト群をまとめ、.check でチェックアイコンを右端に配置しています。.carddisplay: flex にして justify-content: space-between を指定することで、テキストと丸アイコンを左右に分けて配置しています。

<span class="card">
  <span class="text">
    <span class="title">フリー</span>
    <span class="price">¥0 / 月</span>
  </span>
  <span class="check">
    <!-- SVG -->
  </span>
</span>

ポイント2:孫要素のスタイルを切り替える

パターン2では input:checked + .card.card 直下のスタイルを変えていましたが、このパターンでは .card の中にある .check(孫要素)のスタイルも変える必要があります。input:checked + .card .check のように、+.card を参照したあと、さらにスペースで .check を指定することで孫要素にも届かせることができます。

.radio-check-list input:checked + .card .check {
  background: #058ad5;
  border-color: #058ad5;
}

ポイント3:チェックアイコンの表示を切り替える

チェックアイコンのSVGは、通常時は display: none で非表示にしておき、選択時だけ display: block に切り替えて表示します。

.radio-check-list .check svg {
  display: none;
}

.radio-check-list input:checked + .card .check svg {
  display: block;
}

ポイント4:装飾SVGをスクリーンリーダーから隠す

このSVGは視覚的な装飾のためのアイコンであり、スクリーンリーダーに読み上げさせる必要はありません。aria-hidden="true" を付けることで、スクリーンリーダーの読み上げ対象から除外しています。

<svg aria-hidden="true">...</svg>

まとめ

この記事では、CSSでラジオボタンをカスタマイズする方法を3つのパターンで紹介しました。

  • パターン1 — シンプル丸型
    デフォルトに近い構造を保ちながら、見た目をスッキリ整えたいときに
  • パターン2 — カード型
    選択肢を視覚的に強調したいとき、ボタン感のあるUIにしたいときに
  • パターン3 — チェックマーク付きカード
    情報量が多い選択肢を整理して見せたいとき、料金プランなどに

どのパターンも、JavaScriptなしでCSSだけで実装できます。visually-hiddenやアクセシビリティへの配慮も取り入れているので、そのままプロジェクトに組み込んで使ってみてください。

押していただけると励みになります!

ABOUT ME
みなと
みなと
フロントエンドエンジニア
東京のWeb制作会社で15年以上働いている現役フロントエンドエンジニアです。これまで、いろんなプロジェクトに関わりながら、フロントエンド開発やWebデザインに取り組んできました。このブログでは、今までの経験を活かして、Web制作に役立つ情報やノウハウをシェアしていきたいと思います。初心者の方から、現場で働く方まで、誰でも参考にできる内容をお届けしますので、ぜひ覗いてみてください。
記事URLをコピーしました