button要素のtype属性について気にしたくないけどそうもいかない話
この記事はyamanoku Advent Calendar 2023の4日目の記事になります。
皆さんは<button>
要素を書く時にtype
属性を書くこと気にしてますか?
私は気にしてます。気にしすぎているほどに。
なぜ気にしているのか
<button>
要素にはtype
属性があります。このtype
属性はデフォルトの値がsubmit
になっています。送信目的ではなくJavaScriptと組み合わせたインタラクションが目的(例えば要素を開閉するなどの挙動)の場合はtype
属性をbutton
とする必要があります。その他にもフォーム内容をリセットさせるreset
という値もあります。
<button type="submit">送信する</button>
<button type="button">開閉する</button>
<button type="reset">リセットする</button>
この指定がない場合に困る問題としてコンポーネントとして扱っている際に親要素に<form>
要素があると送信ボタンとして認識されて実装時に予期せぬ挙動になることがあります。
alertを出したあと一旦console.logするだけにしたのですが
なぜか画面がリロードされてしまう。。。(URLの最後に?がつくのでget通信しようとしているようにみえる)
これ、ひとつのコンポーネントの template で form と button が同時に出てきていたら比較的探しやすいんだけど、コンポーネントが入れ子になっているとかなり見つけにくいということが分かった。
この挙動自体はHTMLの挙動になるのですが、JavaScriptと一緒に扱っている場合はどこに問題があるのか一見して分からなくなる可能性があります。そうしたヒューマンエラーを防ぐためにもtype
属性を明示的に指定することが重要だと思っています。
ただし以下のように<form>
要素の中で使う場合は送信ボタンとして自明なのでその場合はtype
を省略しても問題ないと思っています。
<form method="post">
<label>
名前
<input name="submitted-name" autocomplete="name" />
</label>
<button>保存</button>
</form>
サンプルコードでtype
属性が明示されていないものがある
技術ブログやドキュメントなどのサンプルコードでtype
属性が明示されていないことがあります。私はネット上にある情報を収集するのが好きなので色々なものを見るのですが、ボタンの使い方として明示されていないものを見ると気になってしょうがありません。
以下はSWRのブログ記事のサンプルコードです。type
属性が省略されています。
const { mutate, data } = useSWR('/api/todos')
return <>
<ul>{/* Display data */}</ul>
<button onClick={() => {
mutate(addNewTodo('New Item'), {
optimisticData: [...data, 'New Item'],
})
}}>
Add New Item
</button>
</>
こうした代表的なライブラリのサンプルコードにtype
属性が省略されていることは、問題ないものと錯覚させてしまうかもしれないので個人的には好ましくありません。
もちろん明示的にtype
属性を示しているものもあります。Preactのチュートリアルではtype="submit"
を明示しています。この例はとても良いので見習ってほしいです。
import { h, render, Component } from 'preact';
class App extends Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<form>
<input type="text" />
<button type="submit">Update</button>
</form>
</div>
);
}
}
render(<App />, document.getElementById('app'));
Tutorial | Preact: Fast 3kb React alternative with the same ES6 API. Components & Virtual DOM.
それはそれとして<input type="text">
にラベルが紐づけていないのが気になります。
Linterでtype
属性をつけるよう矯正する
このような属性に関する指摘をいちいちレビュー等で毎度指摘するのは大変なので、できるかぎり機械的に指摘するようにしてもらいたいです。
そこで業務でも私用でも愛用しているマークアップのリンターであるMarkuplintでは以下のようにtype
属性をつけるように強制できるルールを作れます。
{
"rules": {
"required-attr": true,
},
"nodeRules": [
{
"selector": "button",
"rules": {
"required-attr": {
"value": [
{
"name": "type",
"value": ["button", "reset", "submit"]
}
],
"reason": "type指定がないとデフォルトでsubmitの挙動になるので、予期せぬ挙動にならないように意図的に提示してください。"
}
}
]
},
AngularのESLintでは<button>
要素にtype
属性がないと警告されるルールが搭載されています。これはとても良いものです。
<form>
要素を使わなくてもデータ送信ができるようになった
しかし<button>
要素のtype
属性を意識するのは<form>
要素を使う時だけなのかもしれません。事実、現在サーバーにデータを送信するために<form>
要素を使わなくてもJavaScriptを経由して送信することができます。
JavaScriptでHTTPリクエストを送るオブジェクトであるXMLHTTPRequestを使うことで可能になります。
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/todos');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ title: 'New Item' }));
Fetch APIを使う場合でも同様のことが可能になります。
fetch('/api/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title: 'New Item' }),
});
このように<form>
要素を使わなくてもデータを送信できるようになってきています。JavaScriptとWeb APIの進化により送信するための責務がHTMLから離れてきており、<button type="submit">
を意識することは減ってきてしまっているのかと私は推測しています。
しかし最近ではRemixやReact.js(あるいはNext.js)からServer Actionといったものが生まれてきて、JavaScript(この場合はJSX)を書きつつも<form>
要素でデータ送信を扱う事例も増えてきています。
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'
const initialState = {
message: null,
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}
export function AddForm() {
const [state, formAction] = useFormState(createTodo, initialState)
return (
<form action={formAction}>
<label htmlFor="todo">Enter Task</label>
<input type="text" id="todo" name="todo" required />
<SubmitButton />
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
</form>
)
}
これはハイドレーションが完了するまでにフォーム操作ができるようになるユーザー体験としての利点がありますが、そもそもデータを送信するセマンティクス情報として<form>
要素を意識して書くことができるのであれば、フレームワークに準じて書くべきだと思っています。
<button>
自体が汎用的なものになってきている
ところでいまWebブラウザ上で「ボタン」を表現するとしたら、<button>
要素を使って表現する人が多いと思っています。私もそうです。
HTML仕様上では<input>
要素のtype
属性でbutton
を指定することでも同等の挙動になるのでこの書き方でも有効です。ちなみに<button>
の方が後で生まれたもの(HTML4.0より)になります。
<!-- 以下2つは挙動としては妥当なマークアップ -->
<input type="button" value="お気に入りに追加">
<button type="button">お気に入りに追加</button>
<input>
要素のtype
属性でbutton
を指定するとvalue
属性の値がボタンのラベルになります。<button>
要素では中身のテキストがボタンのラベルとなります。
ですがボタンの中で画像やアイコンを差し込むことを想定した場合は<button>
要素のほうが圧倒的に扱いやすいでしょう。
<button type="button">
<svg
width="20"
height="20"
viewBox="0 0 100 100"
role="img"
aria-hidden="true"
>
<polygon
points="50,10 60,40 90,40 65,60 75,90 50,70 25,90 35,60 10,40 40,40"
></polygon>
</svg>
<span>お気に入りに追加</span>
</button>
最近はインタラクティブな要素としてJavaScriptを書かなくても表現できるものが増えつつあります(現時点でクロスブラウザ対応しているかは置いておきます)。
Popover APIを活用するとJavaScriptを使用せずポップオーバーを表示させることは可能になってきています。
<button type="button" popovertarget="popover">ポップアップを表示する</button>
<div popover id="popover">ポップアップ内のコンテンツ</div>
更に発展させたInvokersを使うとポップオーバーのほかダイアログや開くことも可能になるように検討されています。
<button type="button" invoketarget="my-dialog">ダイアログを開く</button>
<dialog id="my-dialog">ダイアログ内のコンテンツ</dialog>
ブラウザで扱うものが文書としてのWebページ以外にもアプリケーションとしても活用されるようになってきた現代においては、様々なインタラクションの発火点として<button>
要素はますます重宝されることになると思っています。だからこそインタラクションのものとしてtype="button"
を明示的に書くことは重要だと考えています。
type
属性の値は今後増えていくかもしれない
type
属性で使える値はsubmit
とbutton
とreset
のみが今のHTMLの仕様で定義されていますが、今後それらが増えていく可能性があることも頭の片隅に置いておくべきだと思っています。
Web Share API
Web Share APIとはOSの共有機能をWebブラウザから利用できるようにするAPIです。現在この機能を<button>
上で実現するためにtype="share"
という値が提案されています。
<button type="share">共有する</button>
Selectlist Element
OpenUIはW3Cコミュニティグループの1つで、組み込み用のUIコンポーネントのスタイリングや機能拡張を目的として日々活動しています。その中で<selectlist>
要素というものも提案されています。
リストボックスを開くために<button>
要素を使うことができて、その際にtype="selectlist"
という値が使えることが提案されています。
<selectlist>
<button type="selectlist">
selected option: <selectedoption></selectedoption>
</button>
<option>one</option>
<option>two</option>
</selectlist>
これらはいずれもWeb標準の挙動としてはまだ確立されていませんが、type
属性の値が3つしかないと思いこんでいると、今後増えていくことを見落としてしまうかもしれません。
おわりに
以上<button>
要素のtype
属性について書きました。この記事を読んでそもそも<button>
要素のデフォルト挙動がsubmit
になっていること自体が煩わしいと感じた人もいるかもしれません。
しかしWebは後方互換性を重視しているものです。これまで明示的に書いていなかったものがデフォルトの挙動を変えてしまえば何らかのサイトやアプリケーションで動かなくなってしまう可能性が大いにあります。一斉に皆で変えることができればいいでしょうがそんなことは不可能でしょう。Webが一般的になった今、デフォルトの挙動を変えることはとても難しいことです。
<button>
要素のtype
属性の値自体は、マシンリーダブルの観点や支援技術(スクリーンリーダー)の読み上げにおいて影響は及ぼしません(私が知る限り)。しかし私はHTMLの意味論的ルールの観点においても、<button>
要素のtype
属性を実装する人が何の意図をもってそのtype
属性で実装したのか明示的に書くことは、ほかの実装者がそれを知れるためにも重要だと思っています。
Webサイト・アプリケーションを作っていく上で今後も<button>
要素は重要な役割を担っていくと思っています。そのためにもtype
属性を明示的に書いていくことは重要です。これまで意識して書いてなかった人もこれを機に意識して書いていけるようになってくれると嬉しいです。