buttonはdivで代用できる
TL;DR(buttonをdivで代用はできるが、やるなら覚悟が必要)
button[type="button"]
と同等のものをdiv
で代用は可能だが、実装には覚悟が必要。アクセシビリティの問題やフォーカススタイルの追加、キーボードイベントの実装など、ネイティブのbutton
要素と同等の機能を実装するためには多くの工夫が必要となるためである。
通常のボタン
<button type="button" onClick="alert('Clicked!')">Pure Div Button</button>
divで代用したボタン
import { useState, ReactNode, useCallback, KeyboardEvent, MouseEvent, HTMLAttributes } from 'react';
import { createRoot } from 'react-dom/client';
const Button = ({ onClick, children, disabled = false, ...props }: ButtonProps) => {
// キーボードイベントのハンドラー
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLDivElement>) => {
if (disabled) return;
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
onClick?.(e as unknown as MouseEvent<HTMLDivElement>);
}
},
[onClick, disabled],
);
const handleClick = (e: MouseEvent<HTMLDivElement>) => {
if (disabled) return;
onClick?.(e);
};
return (
<div
{...props}
role="button"
tabIndex={disabled ? -1 : 0}
onClick={handleClick}
onKeyDown={handleKeyDown}
aria-disabled={disabled}
aria-pressed={isPressed}
className={`
inline-block px-4 py-2
text-sm
bg-blue-500 text-white
rounded cursor-pointer select-none
transition-all duration-200
/* ホバー時のスタイル */
hover:bg-blue-600
/* フォーカス時のスタイル */
focus:outline-none
focus:ring-2
focus:ring-blue-400
focus:ring-offset-2
/* アクティブ時のスタイル */
active:bg-blue-700
/* 非活性時のスタイル */
${
disabled &&
`
bg-gray-200
text-gray-400
cursor-not-allowed
hover:bg-gray-200
hover:shadow-none
focus:ring-0
focus:ring-offset-0
pointer-events-none
opacity-80
`
}
`}
>
{children}
</div>
);
};
const App = () => {
return (
<>
<Button onClick={() => alert('Clicked!')}>Pure Div Button</Button>
<Button disabled onClick={() => alert('Clicked!')}>
Pure Div Button (disabled)
</Button>
</>
);
};
const rootElement = document.getElementById('root');
if (rootElement) {
const root = createRoot(rootElement);
root.render(<App />);
}
※スタイルは説明を割愛するためTailwind CSSを使用している。
button
要素と同等の機能をある程度同じものを実装するだけでもこれぐらいは実装しなければならない。
解説
button type="button"
の代わりにdiv
を使って同等の機能を実装するためには、以下の考慮が必要となる。
アクセシビリティの問題
button
要素はインタラクティブな要素なため、フォーカスが可能である。一方でdiv
要素は、デフォルトでインタラクティブな要素ではないため、フォーカスはできず、スクリーンリーダーなどの支援技術から適切に認識されない。
role属性の設定
role="button"
を設定するとスクリーンリーダーなどの支援技術に対して要素をボタンとして識別する。
<div role="button"></div>
フォーカス可能な設定
button
要素はデフォルトでフォーカス可能だが、div
だとデフォルトではフォーカスされない。それはrole="button"
を設定しても変わらない。そのため、tabindex="0"
を設定することでキーボードでのフォーカス移動が可能な状態にする。
<div role="button" tabindex="0"></div>
// リアクティブにする場合
const Button = ({ disabled }) => {
return <div role="button" tabIndex={disabled ? -1 : 0} />;
};
tabIndex
の値については、-1
はフォーカス可能だがタブキーでのフォーカス移動ができない状態、0
はタブキーでのフォーカス移動が可能な状態を表す。
フォーカススタイルの追加
div要素はデフォルトでフォーカススタイルを持っていない。キーボードナビゲーションを使用するユーザーがどの要素にフォーカスしているかを視覚的に示すためには、カスタムのフォーカススタイルを追加する必要がある。
div:focus {
outline: 2px solid blue;
}
キーボードイベント
button
要素はEnter
やSpace
キーで操作できることが期待される。しかし、div
要素にrole="button"
を付与しただけではEnter
やSpace
キーで操作ができない。
Enter
やSpace
キーが押された時にクリックイベントを発火させるためには、onKeyDown
イベントでこれらのキーが押された際にクリックイベントを発火させる必要がある。
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault(); // デフォルトのスクロールなどを防止
onClick?.(e as unknown as MouseEvent); // クリックイベント発火
}
},
[onClick],
);
合わせてクリックイベント(onClick()
)が必要であれば、onKeyDown
イベントを受け付けるようにしておく。
const handleClick = (e: MouseEvent) => {
if (disabled) return; // disabled状態の場合、onClickの指定に関わらず抜ける
onClick?.(e);
};
return (<div onClick={handleClick}>)
その他
ARIA Authoring Practices Guide (APG)のButton Patternによるとキーボード操作には以下の要件も求められるようである。
When the button has focus:
- Space: Activates the button.
- Enter: Activates the button.
- Following button activation, focus is set depending on the type of action the button performs. For example:
- If activating the button opens a dialog, the focus moves inside the dialog. (see dialog pattern)
- If activating the button closes a dialog, focus typically returns to the button that opened the dialog unless the function performed in the dialog context logically leads to a different element. For example, activating a cancel button in a dialog returns focus to the button that opened the dialog. However, if the dialog were confirming the action of deleting the page from which it was opened, the focus would logically move to a new context.
- If activating the button does not dismiss the current context, then focus typically remains on the button after activation, e.g., an Apply or Recalculate button.
- If the button action indicates a context change, such as move to next step in a wizard or add another search criteria, then it is often appropriate to move focus to the starting point for that action.
- If the button is activated with a shortcut key, the focus usually remains in the context from which the shortcut key was activated. For example, if Alt + U were assigned to an "Up" button that moves the currently focused item in a list one position higher in the list, pressing Alt + U when the focus is in the list would not move the focus from the list.
視覚的な押下状態の設定
aria-disabled
aria-disabled
は、要素が無効状態であることをスクリーンリーダーに伝える。
aria-disabled={disabled}
disabled
状態の場合は、aria-disabled="true"
を設定する。
もし、さらにtype="submit"
を考慮する場合
ここまでbutton type="button"
の仕様をベースにdiv
を再現しようとしていたが、仮にbutton type="submit"
も考慮する場合は他にも考慮が必要になる。
type="submit"
をサポートする場合に考慮するものの1つとして、フォーム内でEnterキー押下時に自動的に送信される機能がある。
フォーム要素の送信処理
div
がクリックされた際、またはキーボードでのEnterキー押下時に、フォーム全体をsubmit
する処理onClick
やonKeyDown
イベントで、親フォームのsubmit
関数を呼び出す
親フォームの参照取得
Reactの場合は
useRef
を使いdiv
要素から親フォームを取得できるようにするclosest('form')
を使って親フォームを動的に見つけ、そのフォームのsubmit()
メソッドを呼び出すconst handleClick = (e: MouseEvent) => { const form = (e.target as HTMLElement).closest('form'); if (form) { form.requestSubmit(); // フォームを送信 } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); const form = (e.target as HTMLElement).closest('form'); form?.requestSubmit(); } };
JavaScript の場合は、親にさかのぼって
form
要素を取得し、submit()
メソッドを呼び出すconst handleClick = (e: MouseEvent) => { const form = (e.target as HTMLElement).closest('form'); form?.submit(); // フォームを送信 };
このように単なるbutton type="button"
とは異なり、button type="submit"
の場合はフォームの送信処理を考慮する必要がある。
まとめ
代用するためにはボタン要素にrole="button"
を付与するだけでは解決せず、先述までのようにいくつも考慮する点がある。そのため、可能な限りネイティブのボタンを使用することが望ましい。当然ながらネイティブのボタンは、すべてのブラウザと支援技術でサポートされており、カスタマイズなしでキーボードとフォーカスの要件を満たしている。