自作セレクトをやめる日 — Safari 27のbase-selectでアクセシブルな装飾セレクトを作る
ktkt更新 2026.09.17約13分で読めます

自作セレクトをやめる日 — Safari 27のbase-selectでアクセシブルな装飾セレクトを作る

技術メモ

フォームのセレクトボックスを「ちょっとおしゃれにしたい」。そう思って appearance: none で矢印を消し、カスタム矢印を足し、物足りなくなって div と button と ul で自作セレクトを組み立てる。フロントエンドなら一度は通る道です。そして決まって同じ事故が起きます。Tabでフォーカスが迷子になり、矢印キーで選択肢が動かず、スクリーンリーダーが「これは何の部品?」と黙り込む。見た目は整ったのに、キーボード操作と読み上げが崩壊する定番事故です。Safari 27では appearance: base-select と <selectedcontent> に対応し、Chrome 135以降にあった装飾セレクトの仕組みに追いつきました。この記事では自作セレクトを捨てて、ネイティブのまま装飾する最小構成を紹介します。

Before: div自作セレクトのつらさ

自作セレクトがつらい理由は、見た目の再現ではなく「ネイティブが無料でやっていた仕事」を全部引き受けることになる点です。ネイティブの select は、フォーカス移動、EnterやSpaceでの開閉、上下キーでの選択肢移動、Escでのキャンセル、フォーム送信時の値の持ち運び、IME確定との兼ね合い、スクリーンリーダーへの「これは選択肢が何個あるリストボックスです」という自己紹介まで肩代わりしています。div で組み直すと、その全部を role="listbox" や aria-activedescendant、キーイベントの分岐で再実装することになります。

HTML
<!-- 自作セレクトの典型: ここから再実装地獄が始まる -->
<div class="custom-select" role="listbox" tabindex="0">
  <div class="custom-select__current">プランを選択</div>
  <ul class="custom-select__list">
    <li role="option" aria-selected="false">個人プラン</li>
    <li role="option" aria-selected="false">チームプラン</li>
    <li role="option" aria-selected="false">法人プラン</li>
  </ul>
</div>

上の断片だけでも、読み上げ対応には aria-expanded の切り替え、選択中項目の通知、リスト外クリックでの閉鎖、フォーカスを奪いすぎない開閉管理が必要です。矢印キー、HomeやEnd、文字キーでの先頭一致ジャンプまで揃えると、ちょっとしたコンポーネントライブラリ1個分の仕事量になります。しかもOSや支援技術の組み合わせごとに挙動検証が必要で、直したつもりの修正が別の環境で壊れる。見た目のためにアクセシビリティの土台を捨てる取引は、2026年の今は割に合いません。

ネイティブのselectはなぜ装飾しにくかったの?→ ドロップダウンのポップアップ部分がブラウザの影の世界(UAシャドウDOM)に閉じ込められ、CSSが届かなかったからです。base-selectはその壁に公式の窓を開け、選択中表示やポップアップにスタイルを当てられるようにした仕組みです。

After: base-selectの最小実装

appearance: base-select を指定すると、select は「ネイティブの振る舞い(キーボード操作・読み上げ・フォーム連動)はそのまま、中身の見た目だけCSSで触れる」モードに切り替わります。選択中の表示を差し替える専用要素 <selectedcontent> と、ポップアップ全体を指す疑似要素 ::picker(select) が入口です。JavaScriptは不要で、まずはこの3点だけ覚えれば動きます。

CSS
/* 最小構成: ネイティブのまま、見た目だけ取り替える */
.plan-select {
  appearance: base-select;
  font: inherit;
  border: 1px solid #3b5ba9;
  border-radius: 8px;
  padding: 8px 12px;
  background: #fff;
  color: #17202b;
}

/* 選択中の表示部分 */
.plan-select > button > selectedcontent {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* ドロップダウン(ポップアップ)全体 */
.plan-select::picker(select) {
  border: 1px solid #3b5ba9;
  border-radius: 8px;
  padding: 4px;
  margin-top: 4px;
}

/* 選択肢とホバー・選択状態 */
.plan-select option {
  padding: 8px 12px;
  border-radius: 6px;
}
.plan-select option:hover,
.plan-select option:focus-visible {
  background: #f0f3f5;
}
.plan-select option:checked {
  background: #e4eaf4;
  font-weight: 700;
}
HTML
<!-- selectedcontent が「選ばれている中身」をそのまま映す -->
<label for="plan">プランを選択</label>
<select id="plan" class="plan-select">
  <button>
    <selectedcontent></selectedcontent>
  </button>
  <option value="" selected>プランを選択</option>
  <option value="personal">個人プラン</option>
  <option value="team">チームプラン</option>
  <option value="corp">法人プラン</option>
</select>

ポイントは2つです。第一に、<button> の中に置いた <selectedcontent> が、選ばれた option の中身を自動で映します。アイコン入りの選択肢を作れば、選択中表示にも同じアイコンが出ます。第二に、ポップアップ側は ::picker(select) で枠や余白を整え、中の option は通常のCSSで装飾できます。開閉・矢印キー・Esc・フォーム送信はネイティブのままなので、自作時代のキー分岐やARIA管理は丸ごと不要になります。既存の select にクラスと数行のCSSを足すだけなので、置き換えも段階的に進められます。

下のデモはこの記事を開いたブラウザでそのまま動きます。「実行」を押すと、対応ブラウザでは装飾版、未対応ブラウザでは素朴版が表示され、どちらが正解かも判定表示します。選択肢を変えるとフォーム値の連動も確認できます。

HTML ▸ DEMO
<div id="bs1-wrap" style="font-family:sans-serif;max-width:420px;">
  <p id="bs1-status" style="font-size:13px;padding:8px 12px;border-radius:6px;background:#f0f3f5;">判定中…</p>
  <label for="bs1-plan" style="display:block;font-size:14px;font-weight:700;margin:12px 0 6px;">プランを選択</label>
  <select id="bs1-plan" class="bs1-select">
    <button><selectedcontent></selectedcontent></button>
    <option value="" selected>プランを選択</option>
    <option value="personal">個人プラン — 月額500円</option>
    <option value="team">チームプラン — 月額2,000円</option>
    <option value="corp">法人プラン — 個別見積</option>
  </select>
  <p style="font-size:13px;">選択中: <output id="bs1-out">(未選択)</output></p>
</div>
<style>
  .bs1-select {
    font: inherit;
    border: 2px solid #3b5ba9;
    border-radius: 8px;
    padding: 8px 12px;
    background: #fff;
    color: #17202b;
    max-width: 100%;
  }
  @supports (appearance: base-select) {
    .bs1-select { appearance: base-select; }
    .bs1-select::picker(select) {
      border: 2px solid #3b5ba9;
      border-radius: 8px;
      padding: 4px;
    }
    .bs1-select option:checked {
      background: #e4eaf4;
      font-weight: 700;
    }
  }
</style>
<script>
  (() => {
    const ok = CSS.supports('appearance', 'base-select');
    const st = document.getElementById('bs1-status');
    st.textContent = ok
      ? 'このブラウザは base-select に対応:装飾版で表示中'
      : 'このブラウザは base-select 未対応:素朴な表示が正解';
    st.style.background = ok ? '#e4eaf4' : '#fdf0e6';
    const sel = document.getElementById('bs1-plan');
    const out = document.getElementById('bs1-out');
    sel.addEventListener('change', () => {
      const opt = sel.options[sel.selectedIndex];
      out.textContent = sel.value ? opt.text + ' [' + sel.value + ']' : '(未選択)';
    });
  })();
</script>

optionの中に画像や小さな説明文を入れると、selectedcontentにも同じ中身が映ります。選択中は短いラベル、候補一覧では説明付きにしたい場合は、option内の補足要素にクラスを付けて selectedcontent 側だけ非表示にする書き方が定番です。

Chromeではどうなの?

この仕組み自体は目新しいものではなく、Chromeでは去年から使えていました。Safari 27が追いついたことで、主要2エンジンで動く実用段階に入ったのが今回のニュースです。現状の対応は次の通りです。

ブラウザ

対応状況

備考

Chrome / Edge

135以降で対応

selectedcontent・::picker(select)ともに利用可

Safari

27以降で対応

今回の追いつきで2社体制に

Firefox

既定OFFで未対応

フラグ付きの実験段階、@supportsフォールバック必須

詳しい対応状況は caniuse: appearance: base-select で確認できます。結論はシンプルです。Chromeでは去年から使えたものにSafariが追いついて2社になった。Firefoxはまだ既定OFFなので、次の章の @supports フォールバックを前提にした段階導入が現実解です。対応ブラウザでは装飾セレクト、非対応では素朴なネイティブ表示。その落差を許容できるフォームから入れていくのが安全です。

フォールバック: @supportsで段階導入

非対応ブラウザでは appearance: base-select が無視され、<selectedcontent> や ::picker(select) の指定も効きません。そこで基本は素朴なネイティブの見た目にしておき、対応ブラウザだけ上書きする書き方にします。判定は @supports (appearance: base-select) の一行です。

CSS
/* 基本: どのブラウザでも壊れない素朴な見た目 */
.plan-select {
  font: inherit;
  border: 1px solid #3b5ba9;
  border-radius: 8px;
  padding: 8px 12px;
  background: #fff;
  color: #17202b;
}

/* 対応ブラウザだけ装飾モードに切り替え */
@supports (appearance: base-select) {
  .plan-select {
    appearance: base-select;
  }
  .plan-select::picker(select) {
    border: 1px solid #3b5ba9;
    border-radius: 8px;
    padding: 4px;
    margin-top: 4px;
  }
  .plan-select option:checked {
    background: #e4eaf4;
    font-weight: 700;
  }
}

この順番が大切です。@supports の外側に「非対応でも困らない見た目」を置き、内側にだけ新機能の指定を閉じ込めます。こうすればFirefoxの既定設定でもフォームは普通に使え、ChromeとSafari 27以降では装飾版が表示されます。導入時は影響の小さい問い合わせフォームの1個から始め、選択肢にアイコンや説明文を入れたい箇所へ広げるのが無難です。

非対応ブラウザでの見え方確認は、@supportsの中身をコメントアウトするだけで再現できます。リリース前の実機チェックでは「装飾版の見た目」より「素朴版で注文や送信が詰まらないこと」を先に確認すると事故が減ります。

まとめ

div自作セレクトの見た目の自由は、キーボード操作と読み上げの再実装コストと引き換えでした。appearance: base-select と <selectedcontent>、::picker(select) を使えば、ネイティブの振る舞いを保ったまま見た目だけ取り替えられます。Safari 27で2社対応になった今、@supports を添えた段階導入が現実解です。

参考リンク

仕様の裏取りと最新状況は一次情報をどうぞ。Safari側の対応内容は Safari 27 Release Notes、プロパティの定義と各ブラウザの対応表は MDN: appearance と caniuse: appearance: base-select が確実です。

SERIES

Safari 27の新機能を実装で確認

Safari 27で使えるようになったCSS・アクセシビリティ・画像最適化の機能を、コードとフォールバックつきで確認する特集です。Chromeとの対応差がある機能は、無理に全面採用せず「使う場面」を限定して紹介しています。

シリーズの記事を読む(4本)
シェア
ktkt
ktkt運営者

フルスタックWebエンジニア。モダンなフロントエンドとAPI開発が得意。 Next.jsを中心にWebアプリ制作、AI活用支援、動画編集など活動中。 複雑な技術を、使える道具にデコードする。

タグで探す

関連記事

同じタグ・カテゴリの記事から選んでいます。