カスタムセレクト
ネイティブselectを装飾しなおしたドロップダウン。開閉アニメーションと選択反映に加え、矢印キーやEnterでの操作にも対応します。
ライブデモ
コード
HTML
<div class="stage">
<div class="cs-card">
<span class="cs-label" id="cs-label">お届け方法</span>
<div class="cs-select">
<button type="button"
class="cs-trigger"
id="cs-trigger"
aria-haspopup="listbox"
aria-expanded="false"
aria-labelledby="cs-label cs-trigger">
<span class="cs-trigger-text" id="cs-trigger-text">選択してください</span>
<span class="cs-arrow" aria-hidden="true"></span>
</button>
<ul class="cs-list"
id="cs-list"
role="listbox"
tabindex="-1"
aria-labelledby="cs-label"
hidden>
<li class="cs-option" role="option" id="cs-opt-0" data-value="standard" aria-selected="false">通常配送(3〜5日)</li>
<li class="cs-option" role="option" id="cs-opt-1" data-value="express" aria-selected="false">お急ぎ便(翌日到着)</li>
<li class="cs-option" role="option" id="cs-opt-2" data-value="pickup" aria-selected="false">店舗受け取り</li>
<li class="cs-option" role="option" id="cs-opt-3" data-value="locker" aria-selected="false">宅配ロッカー受け取り</li>
</ul>
</div>
<p class="cs-status" id="cs-status" role="status">未選択です</p>
</div>
</div>
CSS
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
/* 上品なティール系グラデ背景 */
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 50%, #cffafe 100%);
color: #1f2937;
}
.stage { width: 100%; display: grid; place-items: center; }
.cs-card {
width: min(340px, 92vw);
padding: 28px 26px 24px;
background: #fff;
border-radius: 20px;
box-shadow: 0 24px 56px -26px rgba(6, 95, 70, 0.32);
}
.cs-label {
display: block;
margin-bottom: 8px;
font-size: 0.8rem;
font-weight: 600;
color: #065f46;
letter-spacing: 0.02em;
}
.cs-select { position: relative; }
.cs-trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 13px 16px;
font-size: 0.92rem;
font-family: inherit;
color: #0f172a;
background: #f0fdfa;
border: 2px solid #a7f3d0;
border-radius: 12px;
cursor: pointer;
text-align: left;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.cs-trigger:hover { border-color: #6ee7b7; }
.cs-trigger:focus-visible,
.cs-trigger[aria-expanded="true"] {
outline: none;
border-color: #059669;
box-shadow: 0 0 0 4px rgba(5, 150, 105, 0.16);
}
.cs-trigger-text { color: #9aa5a1; }
.cs-trigger.has-value .cs-trigger-text { color: #0f172a; font-weight: 600; }
.cs-arrow {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid #059669;
border-bottom: 2px solid #059669;
transform: rotate(45deg);
transition: transform 0.22s ease;
}
.cs-trigger[aria-expanded="true"] .cs-arrow { transform: rotate(-135deg); }
.cs-list {
position: absolute;
z-index: 5;
left: 0;
right: 0;
top: calc(100% + 8px);
margin: 0;
padding: 6px;
list-style: none;
background: #fff;
border-radius: 14px;
box-shadow: 0 20px 44px -16px rgba(6, 95, 70, 0.35);
border: 1px solid #d1fae5;
transform-origin: top center;
opacity: 0;
transform: translateY(-6px) scale(0.98);
transition: opacity 0.16s ease, transform 0.16s ease;
max-height: 220px;
overflow-y: auto;
}
.cs-list[hidden] { display: none; }
.cs-list.is-open {
display: block;
opacity: 1;
transform: translateY(0) scale(1);
}
.cs-option {
padding: 10px 12px;
border-radius: 9px;
font-size: 0.88rem;
color: #1e293b;
cursor: pointer;
transition: background 0.14s ease, color 0.14s ease;
}
.cs-option:hover,
.cs-option.is-active {
background: #d1fae5;
}
.cs-option[aria-selected="true"] {
color: #047857;
font-weight: 700;
}
.cs-option[aria-selected="true"]::after {
content: "✓";
float: right;
}
.cs-status {
margin: 14px 0 0;
font-size: 0.76rem;
color: #6b7d78;
}
@media (prefers-reduced-motion: reduce) {
.cs-arrow, .cs-list, .cs-option { transition: none; }
}
JavaScript
// カスタムセレクト:listboxパターンでクリック/キーボード双方の操作に対応する
(function () {
var trigger = document.getElementById("cs-trigger");
var triggerText = document.getElementById("cs-trigger-text");
var list = document.getElementById("cs-list");
var status = document.getElementById("cs-status");
var select = trigger ? trigger.closest(".cs-select") : null;
if (!trigger || !triggerText || !list || !status || !select) return;
var options = Array.prototype.slice.call(list.querySelectorAll(".cs-option"));
if (!options.length) return;
var activeIndex = -1;
var selectedIndex = -1;
function open() {
list.hidden = false;
list.classList.add("is-open");
trigger.setAttribute("aria-expanded", "true");
activeIndex = selectedIndex >= 0 ? selectedIndex : 0;
setActive(activeIndex);
list.focus();
}
function close(focusTrigger) {
list.hidden = true;
list.classList.remove("is-open");
trigger.setAttribute("aria-expanded", "false");
activeIndex = -1;
options.forEach(function (opt) { opt.classList.remove("is-active"); });
list.removeAttribute("aria-activedescendant");
if (focusTrigger) trigger.focus();
}
function isOpen() {
return trigger.getAttribute("aria-expanded") === "true";
}
function setActive(index) {
options.forEach(function (opt, i) {
opt.classList.toggle("is-active", i === index);
});
var current = options[index];
if (current) {
list.setAttribute("aria-activedescendant", current.id);
current.scrollIntoView({ block: "nearest" });
}
}
function selectOption(index) {
var opt = options[index];
if (!opt) return;
selectedIndex = index;
options.forEach(function (o) { o.setAttribute("aria-selected", "false"); });
opt.setAttribute("aria-selected", "true");
triggerText.textContent = opt.textContent;
trigger.classList.add("has-value");
status.textContent = opt.textContent + " を選択しました";
close(true);
}
trigger.addEventListener("click", function () {
if (isOpen()) { close(true); } else { open(); }
});
trigger.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
e.preventDefault();
open();
}
});
list.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown") {
e.preventDefault();
activeIndex = (activeIndex + 1) % options.length;
setActive(activeIndex);
} else if (e.key === "ArrowUp") {
e.preventDefault();
activeIndex = (activeIndex - 1 + options.length) % options.length;
setActive(activeIndex);
} else if (e.key === "Home") {
e.preventDefault();
activeIndex = 0;
setActive(activeIndex);
} else if (e.key === "End") {
e.preventDefault();
activeIndex = options.length - 1;
setActive(activeIndex);
} else if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (activeIndex >= 0) selectOption(activeIndex);
} else if (e.key === "Escape") {
e.preventDefault();
close(true);
} else if (e.key === "Tab") {
close(false);
}
});
options.forEach(function (opt, index) {
opt.addEventListener("click", function () { selectOption(index); });
opt.addEventListener("mouseenter", function () {
activeIndex = index;
setActive(activeIndex);
});
});
document.addEventListener("click", function (e) {
if (!select.contains(e.target) && isOpen()) close(false);
});
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「カスタムセレクト」の効果を追加してください。
# 追加してほしい効果
カスタムセレクト(フォーム & 入力)
ネイティブselectを装飾しなおしたドロップダウン。開閉アニメーションと選択反映に加え、矢印キーやEnterでの操作にも対応します。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="stage">
<div class="cs-card">
<span class="cs-label" id="cs-label">お届け方法</span>
<div class="cs-select">
<button type="button"
class="cs-trigger"
id="cs-trigger"
aria-haspopup="listbox"
aria-expanded="false"
aria-labelledby="cs-label cs-trigger">
<span class="cs-trigger-text" id="cs-trigger-text">選択してください</span>
<span class="cs-arrow" aria-hidden="true"></span>
</button>
<ul class="cs-list"
id="cs-list"
role="listbox"
tabindex="-1"
aria-labelledby="cs-label"
hidden>
<li class="cs-option" role="option" id="cs-opt-0" data-value="standard" aria-selected="false">通常配送(3〜5日)</li>
<li class="cs-option" role="option" id="cs-opt-1" data-value="express" aria-selected="false">お急ぎ便(翌日到着)</li>
<li class="cs-option" role="option" id="cs-opt-2" data-value="pickup" aria-selected="false">店舗受け取り</li>
<li class="cs-option" role="option" id="cs-opt-3" data-value="locker" aria-selected="false">宅配ロッカー受け取り</li>
</ul>
</div>
<p class="cs-status" id="cs-status" role="status">未選択です</p>
</div>
</div>
【CSS】
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
/* 上品なティール系グラデ背景 */
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 50%, #cffafe 100%);
color: #1f2937;
}
.stage { width: 100%; display: grid; place-items: center; }
.cs-card {
width: min(340px, 92vw);
padding: 28px 26px 24px;
background: #fff;
border-radius: 20px;
box-shadow: 0 24px 56px -26px rgba(6, 95, 70, 0.32);
}
.cs-label {
display: block;
margin-bottom: 8px;
font-size: 0.8rem;
font-weight: 600;
color: #065f46;
letter-spacing: 0.02em;
}
.cs-select { position: relative; }
.cs-trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 13px 16px;
font-size: 0.92rem;
font-family: inherit;
color: #0f172a;
background: #f0fdfa;
border: 2px solid #a7f3d0;
border-radius: 12px;
cursor: pointer;
text-align: left;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.cs-trigger:hover { border-color: #6ee7b7; }
.cs-trigger:focus-visible,
.cs-trigger[aria-expanded="true"] {
outline: none;
border-color: #059669;
box-shadow: 0 0 0 4px rgba(5, 150, 105, 0.16);
}
.cs-trigger-text { color: #9aa5a1; }
.cs-trigger.has-value .cs-trigger-text { color: #0f172a; font-weight: 600; }
.cs-arrow {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid #059669;
border-bottom: 2px solid #059669;
transform: rotate(45deg);
transition: transform 0.22s ease;
}
.cs-trigger[aria-expanded="true"] .cs-arrow { transform: rotate(-135deg); }
.cs-list {
position: absolute;
z-index: 5;
left: 0;
right: 0;
top: calc(100% + 8px);
margin: 0;
padding: 6px;
list-style: none;
background: #fff;
border-radius: 14px;
box-shadow: 0 20px 44px -16px rgba(6, 95, 70, 0.35);
border: 1px solid #d1fae5;
transform-origin: top center;
opacity: 0;
transform: translateY(-6px) scale(0.98);
transition: opacity 0.16s ease, transform 0.16s ease;
max-height: 220px;
overflow-y: auto;
}
.cs-list[hidden] { display: none; }
.cs-list.is-open {
display: block;
opacity: 1;
transform: translateY(0) scale(1);
}
.cs-option {
padding: 10px 12px;
border-radius: 9px;
font-size: 0.88rem;
color: #1e293b;
cursor: pointer;
transition: background 0.14s ease, color 0.14s ease;
}
.cs-option:hover,
.cs-option.is-active {
background: #d1fae5;
}
.cs-option[aria-selected="true"] {
color: #047857;
font-weight: 700;
}
.cs-option[aria-selected="true"]::after {
content: "✓";
float: right;
}
.cs-status {
margin: 14px 0 0;
font-size: 0.76rem;
color: #6b7d78;
}
@media (prefers-reduced-motion: reduce) {
.cs-arrow, .cs-list, .cs-option { transition: none; }
}
【JavaScript】
// カスタムセレクト:listboxパターンでクリック/キーボード双方の操作に対応する
(function () {
var trigger = document.getElementById("cs-trigger");
var triggerText = document.getElementById("cs-trigger-text");
var list = document.getElementById("cs-list");
var status = document.getElementById("cs-status");
var select = trigger ? trigger.closest(".cs-select") : null;
if (!trigger || !triggerText || !list || !status || !select) return;
var options = Array.prototype.slice.call(list.querySelectorAll(".cs-option"));
if (!options.length) return;
var activeIndex = -1;
var selectedIndex = -1;
function open() {
list.hidden = false;
list.classList.add("is-open");
trigger.setAttribute("aria-expanded", "true");
activeIndex = selectedIndex >= 0 ? selectedIndex : 0;
setActive(activeIndex);
list.focus();
}
function close(focusTrigger) {
list.hidden = true;
list.classList.remove("is-open");
trigger.setAttribute("aria-expanded", "false");
activeIndex = -1;
options.forEach(function (opt) { opt.classList.remove("is-active"); });
list.removeAttribute("aria-activedescendant");
if (focusTrigger) trigger.focus();
}
function isOpen() {
return trigger.getAttribute("aria-expanded") === "true";
}
function setActive(index) {
options.forEach(function (opt, i) {
opt.classList.toggle("is-active", i === index);
});
var current = options[index];
if (current) {
list.setAttribute("aria-activedescendant", current.id);
current.scrollIntoView({ block: "nearest" });
}
}
function selectOption(index) {
var opt = options[index];
if (!opt) return;
selectedIndex = index;
options.forEach(function (o) { o.setAttribute("aria-selected", "false"); });
opt.setAttribute("aria-selected", "true");
triggerText.textContent = opt.textContent;
trigger.classList.add("has-value");
status.textContent = opt.textContent + " を選択しました";
close(true);
}
trigger.addEventListener("click", function () {
if (isOpen()) { close(true); } else { open(); }
});
trigger.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
e.preventDefault();
open();
}
});
list.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown") {
e.preventDefault();
activeIndex = (activeIndex + 1) % options.length;
setActive(activeIndex);
} else if (e.key === "ArrowUp") {
e.preventDefault();
activeIndex = (activeIndex - 1 + options.length) % options.length;
setActive(activeIndex);
} else if (e.key === "Home") {
e.preventDefault();
activeIndex = 0;
setActive(activeIndex);
} else if (e.key === "End") {
e.preventDefault();
activeIndex = options.length - 1;
setActive(activeIndex);
} else if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (activeIndex >= 0) selectOption(activeIndex);
} else if (e.key === "Escape") {
e.preventDefault();
close(true);
} else if (e.key === "Tab") {
close(false);
}
});
options.forEach(function (opt, index) {
opt.addEventListener("click", function () { selectOption(index); });
opt.addEventListener("mouseenter", function () {
activeIndex = index;
setActive(activeIndex);
});
});
document.addEventListener("click", function (e) {
if (!select.contains(e.target) && isOpen()) close(false);
});
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。