フォロー

🧩 エンジニア豆知識 #5──「select なしでもセレクト!」ラジオ+CSSで作るカスタムドロップダウン完全解説🔥

戦え、エンジニア諸君!!
<select> だとデザインが制限される…」 — そんな悩みをネイティブ要素の組み合わせだけで解決する実装 Tips を紹介する!
キモはラジオボタン+input[type="text"]+CSS
―― 今回は動くサンプルを丸ごと貼りつつ、構造・挙動・アクセシビリティまで一気に押さえるッ!!


🔧 1. サンプル全体

まずは完成版コードをどうぞ。コピペ即動作!

<!-- HTML部分抜粋 -->
<div class="select-wrapper">
  <label for="selectInput1">
    <input type="text" id="selectInput1" readonly placeholder="選択してください">
  </label>
  <div class="radio-dropdown">
    <label class="radio-option">
      <input type="radio" name="option1" value="オプション1-1">
      <span>オプション1-1</span>
    </label>
    ...
  </div>
</div>

(全文コードは記事末に貼付)


💡 2. 仕組みのポイント

  1. 入力欄=ただの <input type="text" readonly>
    →実際にデータを送るのはラジオ。テキストは「表示用」キャッシュ。
  2. 選択肢はラジオ+<span>
    →クリック領域を広げ、CSSで <span> を option 風に装飾。
  3. 開閉制御は .open クラス
    →クリックで付替え、矢印の回転も ::after で演出。

🛠️ 3. 改造ポイント(UXアップ)

  • Esc キーで閉じる document.addEventListener('keydown', e => { if(e.key==='Escape') closeAll(); });
  • ARIA 属性でスクリーンリーダー対応 role="combobox" & aria-expanded を付与。
  • キーボード操作 上下矢印で次の .radio-option に focus → space で選択。

🚨 4. 注意点

  • フォーム送信:実際にサーバへ飛ぶのはラジオ値。
    バリデーションは required をラジオに付与すると楽。
  • 同名グループname="option1"/option2 … を忘れずに。
  • クリックバブリング:ネストが深いと二重発火に要注意。

📜 全コード再掲(HTML + CSS + JS)

コード全文はこちら
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Multiple Custom Radio Dropdowns</title>
    <style>
        .select-wrapper {
            position: relative;
            display: inline-block;
            width: 200px;
            margin: 10px;
        }

        /* 入力フィールドをselect風に */
        input[type="text"] {
            width: 100%;
            padding: 8px 24px 8px 8px; /* 右側に矢印のスペース */
            font-size: 16px;
            border: 1px solid #ccc;
            border-radius: 4px;
            background: #fff;
            cursor: pointer;
            box-sizing: border-box;
            appearance: none;
            -webkit-appearance: none;
            -moz-appearance: none;
        }

        /* ラジオボタンのコンテナをselectのドロップダウン風に */
        .radio-dropdown {
            display: none;
            position: absolute;
            top: 100%;
            left: 0;
            width: 100%;
            background: #fff;
            border: 1px solid #ccc;
            border-radius: 4px;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
            z-index: 10;
            padding: 0;
            max-height: 200px; /* スクロール可能にする */
            overflow-y: auto;
        }

        .radio-dropdown.open {
            display: block;
        }

        /* ラジオボタンを非表示 */
        .radio-option input[type="radio"] {
            display: none;
        }

        /* ラジオオプションをselectのoption風に */
        .radio-option {
            margin: 0;
            padding: 8px 12px;
            cursor: pointer;
            font-size: 16px;
            color: #333;
        }

        /* ホバーと選択時のスタイル */
        .radio-option:hover {
            background: #f0f0f0;
        }

        .radio-option input[type="radio"]:checked + span {
            background: #e0e0e0;
            display: block;
        }

        /* spanでテキストを囲む */
        .radio-option span {
            display: block;
            padding: 0;
        }

        /* カスタム矢印 */
        .select-wrapper::after {
            content: '';
            position: absolute;
            top: 50%;
            right: 10px;
            width: 0;
            height: 0;
            border-left: 5px solid transparent;
            border-right: 5px solid transparent;
            border-top: 5px solid #333;
            transform: translateY(-50%);
            transition: transform 0.2s ease;
            pointer-events: none;
        }

        .select-wrapper.open::after {
            transform: translateY(-50%) rotate(180deg);
        }

        /* ラベルのハイライト */
        .highlight {
            background-color: yellow;
        }
    </style>
</head>
<body>
    <!-- ドロップダウン1 -->
    <div class="select-wrapper">
        <label for="selectInput1">
            <input type="text" id="selectInput1" readonly placeholder="選択してください">
        </label>
        <div class="radio-dropdown">
            <label class="radio-option">
                <input type="radio" name="option1" value="オプション1-1">
                <span>オプション1-1</span>
            </label>
            <label class="radio-option">
                <input type="radio" name="option1" value="オプション1-2">
                <span>オプション1-2</span>
            </label>
            <label class="radio-option">
                <input type="radio" name="option1" value="オプション1-3">
                <span>オプション1-3</span>
            </label>
        </div>
    </div>

    <!-- ドロップダウン2 -->
    <div class="select-wrapper">
        <label for="selectInput2">
            <input type="text" id="selectInput2" readonly placeholder="選択してください">
        </label>
        <div class="radio-dropdown">
            <label class="radio-option">
                <input type="radio" name="option2" value="オプション2-1">
                <span>オプション2-1</span>
            </label>
            <label class="radio-option">
                <input type="radio" name="option2" value="オプション2-2">
                <span>オプション2-2</span>
            </label>
            <label class="radio-option">
                <input type="radio" name="option2" value="オプション2-3">
                <span>オプション2-3</span>
            </label>
        </div>
    </div>

    <script>
        // すべての .select-wrapper を取得
        const wrappers = document.querySelectorAll('.select-wrapper');

        // 各ドロップダウンにイベントを設定
        wrappers.forEach(wrapper => {
            const input = wrapper.querySelector('input[type="text"]');
            const dropdown = wrapper.querySelector('.radio-dropdown');
            const radios = dropdown.querySelectorAll('input[type="radio"]');
            const label = wrapper.querySelector('label');

            // 入力フィールドをクリックしてドロップダウンを開閉
            input.addEventListener('click', (event) => {
                toggleDropdown(wrapper, dropdown, label);
                event.stopPropagation();
            });

            // ラジオボタン選択時
            radios.forEach(radio => {
                radio.addEventListener('change', () => {
                    input.value = radio.value;
                    closeDropdown(wrapper, dropdown, label);
                });
            });

            // ラベルをクリックしたときの動作
            label.addEventListener('click', (event) => {
                toggleDropdown(wrapper, dropdown, label);
                event.stopPropagation();
            });

            // ラジオオプション(span)のクリックで選択
            dropdown.querySelectorAll('.radio-option').forEach(option => {
                option.addEventListener('click', (event) => {
                    const radio = option.querySelector('input[type="radio"]');
                    radio.checked = true;
                    radio.dispatchEvent(new Event('change'));
                    event.stopPropagation();
                });
            });
        });

        // ドキュメント全体のクリックでドロップダウンを閉じる
        document.addEventListener('click', () => {
            wrappers.forEach(wrapper => {
                const dropdown = wrapper.querySelector('.radio-dropdown');
                const label = wrapper.querySelector('label');
                closeDropdown(wrapper, dropdown, label);
            });
        });

        // ドロップダウンを開閉する関数
        function toggleDropdown(wrapper, dropdown, label) {
            if (dropdown.classList.contains('open')) {
                closeDropdown(wrapper, dropdown, label);
            } else {
                // 他のドロップダウンをすべて閉じる
                wrappers.forEach(otherWrapper => {
                    const otherDropdown = otherWrapper.querySelector('.radio-dropdown');
                    const otherLabel = otherWrapper.querySelector('label');
                    closeDropdown(otherWrapper, otherDropdown, otherLabel);
                });
                openDropdown(wrapper, dropdown, label);
            }
        }

        // ドロップダウンを開く
        function openDropdown(wrapper, dropdown, label) {
            dropdown.classList.add('open');
            wrapper.classList.add('open');
            label.classList.add('highlight');
        }

        // ドロップダウンを閉じる
        function closeDropdown(wrapper, dropdown, label) {
            dropdown.classList.remove('open');
            wrapper.classList.remove('open');
            label.classList.remove('highlight');
        }
    </script>
</body>
</html>

📝 6. 覚悟のまとめ

  • ネイティブ要素+CSS+JS少量で自由なUIは作れる
  • select代替はアクセシビリティに目を光らせろ
  • コードを分解→再構築する思考がUI実装力を育てる

📚 さらなる高みへ──団長推薦の学び

CSSシークレット ─ 47のテクニックで CSS をもっと強力に
擬似要素&フィルタで“ありえないUI”を実現するネタ帳。カスタムセレクトの発想源に!

Amazonで見る

※リンクはアフィリエイトを含みます。


🔥 団長の喝──コードを解体し、再構築せよ!

既成のタグに甘えるな。
原理を掴めば、UIは無限にデザインできる。
今日も一行、一スタイル、一イベントを研ぎ澄まし、
戦えッ! そして創れッ!!🔥🔥🔥

コメントする