アクセシブルなボタン
ボタン
code:Button.html
<button>普通のボタン</button>
<button /> はもちろん、<a /> <select /> など大抵の場合はマークアップだけで aria-**** のような呪文を書かなくてもアクセシビリティ対応は可能です。
トグルボタン
トグルの実装自体は平易なので、ここではアクセシビリティ対応によるCSS記述のメリットを紹介します。
code:ToggleButton.tsx
const ToggleButton = ({ pressed = false }) => (
<AppButton
class="toggle-button"
aria-pressed={pressed}
トグルボタン
<span>{ pressed ? 適当なアイコン : 適当なアイコン }</span>
</Button>
)
/** styled-component */
const AppButton = styled.button<HTMLButtonElement | IProps>`
background-color: 'red'
}
/** 全て &[aria-pressed='true' に凝縮できるので不要になる */ background-color: ${props => props.pressed ? 'red' : 'inherit'}
`
/** CSS Modules */
.toggle-button {
/** ='true' は省略できます */
/** classNames(styles['toggle-button'], {[styles['-pressed']: pressd}) が不要になる */
.-pressed {}
}
aria-pressed は undefined(default) | true | false | mixed を持つ属性で、undefined でなければトグルボタンだと認識されます。
CSS では要素の属性に対して [属性=['属性値']] でアクセスできるため、追加のクラスを持たせずとも済みます。また追加のクラスと違って命名が固定なので案件/個人による表記揺れもなくなります。特に大きいのは、視覚的にわかりやすく 処理と CSS の紐付けが行えることでしょうか。たとえば今回のケースだと &[aria-pressed] を基準に &.-fill &.-no-border を付与できます。.-pressed.-fill と .-fill { &.-pressed } と .-pressed { &.-fill } が混在しうるなかで、他にも大量に .-xxx 形式のオプションが追加されたとき、破綻しないよう注意するには労力が掛かります。その点 &[aria-xx]
は明瞭です。
非活性ボタン
code:DisabledButton.html
<button disabled>非活性</button>
aria-disabled もありますが、実際に非活性になるわけではないので実装面の負担が増加します。
リンクボタン
code:LinkButton.html
<a>
<span>リンクボタン</span>
</a>
code:LinkButton.html
<!-- WRONG: a は内部的に role="link" を持ちます。role の上書きは推奨されていません -->
<a role="button">リンクボタン</a>
アコーディオン
ここからは button 要素を含むもの、という括りで紹介していきます。実装コストが少し上がりますが、人によって実装がぶれにくくなる、という点ではメリットがあるかもしれません。
code:Accordion.html
<button
id={accordion-header-${id}}
class={panel}
aria-expanded={isOpen}
aria-controls={accordion-panel-${id}}
onClick={handleClick}
開閉ボタン
</button>
<!-- 意味を持たない div 要素には aria-labelledby は禁止されています -->
<section
id={accordion-panel-${id}}
aria-labelledby={accordion-header-${id}}
hidden={isOpen} /** 将来的に、閉じているケースでは until-found を指定します */
</section>
<!-- 例外的に、アコーディオンの中身が7つ以上のセクションを持つ場合は div で実装します -->
<!-- 記載がない/見つけられないものの、おそらく極端な多重アコーディオンにも同様のことがいえると私は解釈しています -->
aria-expanded は何かを開閉させるような処理を負っているかどうかを示します。
aria-controls は処理の対象を示します。
この2つによって <section /> の開閉処理を司るボタンであることが明示されます。
続いて <section /> 側の説明です。
そもそも <section /> はメインの文書からアコーディオン内部の文章が分離していることを示すために用います。(一部の記事で <div role="region" /> が用いられていますが同じ意味であり、かつ <section /> が望ましいです)
aria-labelledby は別の要素へ関連付けるために用います。
code:Accordion.css
button {
/** 開いているときのスタイル */
}
}
CSS は今までと同様に記述します。
タブ
実のところ、骨組みのイメージは Accordion とほぼ同じです。
違いは <button /> がリストになったのみ。
code:Tab.text
<tablist>
<tab />
<tab />
</tablist>
<tabpanel />
問題は、HTML の標準として tablist などが提供されていないことです。
そのため苦肉の策として role 属性で解決します。
code:Tab.tsx
const TabList = ({ tabList }) => (
<div>
<div role="tablist">
{tabList.map(tabPair => (
<button
role="tab"
id={tab-${tabPair.id}}
aria-controls={tabpanel-${tabPair.id}}
aria-selected={selectedId === ...}
onClick={handleClick}
{tabPair.label}
</button>
))}
</div>
{tabList.map(tabPair => (
<div
role="tabpanel"
id={tabpanel-${tabPair.id}}
aria-labelledby={tab-${tabPair.id}}
hidden={selectedId !== ...}
{tabPair.content}
</div>
))}
</div>
)
aria-controls や aria-hidden はアコーディオンのときと変わりませんが、いくつか追加されている属性があります。
role は役割の解釈を与えるものです。たとえば <a /> はもともと link という暗黙の role を持ちます。今回の <div role="tab" /> はタブという意味を持ち、意味があるゆえに aria-labelledby を付与できます。
aria-selected は現在選択されているかどうかを示します。
tabpannel はあらかじめ全ての div を描画(非表示でもOK)しておかなければならないことに注意してください。ただし将来的に変更される可能性があります。