CSSで、要素に合わせてマウスカーソルの形を変える方法

​​CSS​HTML,Playwright

マウスカーソルの形は、CSS の cursor プロパティで要素ごとに変えられます。cursor: pointer なら指を差した手、cursor: not-allowed なら丸に斜線の形になります。自分で用意した画像も使えますが、そのときは url() の後ろに、画像が使えないときに代わりに使うキーワードを必ず書きます。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。カーソルを実際に画面に出して確かめるため、Chromium をヘッドレスではなく Xvfb の仮想の画面に開き、xdotool で X のマウスポインターを動かしました。表示されたカーソルの画像は X サーバーから取り出し、カーソルテーマ(Adwaita、32px)のどの画像と同じかを調べています。記事の画像に写っているカーソルは、このカーソルテーマの絵柄です。Firefox と Safari、Windows と macOS、タッチ操作の端末は試していません。

cursor にキーワードを書く

書き方は、要素に cursor とキーワードを指定するだけです。

css
.card {
  cursor: pointer;
}

検証環境で、キーワードを1つずつ指定したタイルにマウスを重ね、出たカーソルを撮りました。

15個のタイルを3列に並べた画面。タイルの下にキーワードの名前があり、その上にマウスカーソルが写っている。default は矢印、pointer は指を差した手、text は I の字、not-allowed は丸に斜線、grab は開いた手、grabbing は握った手、move は上下左右の矢印、help は「?」、wait は円、progress は矢印と小さな円、crosshair は細い十字、zoom-in は「+」の入った虫めがね、ew-resize は左右の矢印、ns-resize は上下の矢印。none のタイルにはカーソルが写っていない
キーワードごとのカーソル(Chromium 153、カーソルテーマ Adwaita)

MDN の cursor のページには、キーワードごとの意味が書かれています。よく使うものを抜き出すと、次のとおりです。

キーワード MDN の説明
pointer リンクを示す。ふつうは指差す手の画像
not-allowed 要求された操作が受け付けられない
grab / grabbing 何かをつかんで動かせる/動かしている
wait プログラムがビジー状態で、ユーザーは操作できない
progress バックグラウンドでビジー状態だが、ユーザーは操作できる
help ヘルプが使える
none カーソルを表示しない

wait と progress は、どちらも「処理中」を表しますが、ユーザーが操作できるかどうかが違います。上の画像でも、wait は円の形だけ、progress は矢印に小さな円が付いた形でした。

move を指定したタイルでは、上下左右に矢印の付いた画像が出ました。この画像は、カーソルテーマ Adwaita の中で all-scroll という名前の画像と同じものでした。

CSS を書かないときに出るカーソル

cursor の初期値は auto で、ブラウザが場所に合わせてカーソルを選びます。CSS で何も指定していない要素にマウスを重ねると、次のようになりました。

6つの行を縦に並べた画面。a href の行では、リンクの文字の上に指を差した手。href のない a の行では、文字の上に I の字。button の行と button disabled の行では、ボタンの上に矢印。input type="text" の行では、入力欄の上に I の字。p の文字の行では、文字の上に I の字
CSS を書かないときのカーソル(Chromium 153)
要素 getComputedStyle() の cursor 出たカーソル
<a href> pointer 指を差した手
<a>(href なし) auto 文字の上では I の字
<button> default 矢印
<button disabled> default 矢印
<input type="text"> text I の字
<p> の文字 auto I の字

指の形になったのは、href のあるリンクだけでした。<button> は矢印のままです。Chromium のソースにある既定のスタイルシート(html.css)には、href のある <a> に cursor: pointer、<input> に cursor: text が書かれています。ボタンでも指の形にしたいときは、ボタンに cursor: pointer を書きます。

子の要素にも引き継がれる

CSS の仕様(CSS Basic User Interface Module Level 4)では、cursor は継承されるプロパティです。親の要素に書けば、中の要素にも同じカーソルが出ます。

css
.card {
  cursor: pointer;
}
html
<div class="card">
  <p>カードの文章</p>
  <input type="text" value="カードの中の入力欄">
</div>

カードの中の文章にマウスを重ねると、<p> の cursor は pointer になり、指の形が出ました。一方、入力欄の cursor は text のままで、I の字が出ました。前の節で見たとおり、Chromium の既定のスタイルシートでは <input> に cursor: text が書かれているので、親の pointer は引き継がれませんでした。

無効なボタンに not-allowed を出す

押せないボタンで丸に斜線の形を出したいときは、:disabled に cursor: not-allowed を書きます。

css
button:disabled {
  cursor: not-allowed;
}

disabled を付けたボタンにマウスを重ねると、not-allowed のカーソルが出ました。

ただし、同じボタンに pointer-events: none も付けると、ボタンの cursor は not-allowed なのに、出たカーソルは指の形でした。検証では、cursor: pointer を指定した要素でボタンを囲んでいました。CSS の仕様では、要素が重なっているとき、カーソルを決めるのは、その位置をクリックしたときにクリックを受け取る要素です。MDN の pointer-events のページにあるとおり、pointer-events: none の要素はポインターイベントの対象にならないので、その下の親の要素のカーソルが出ます。

css
/* この組み合わせでは、not-allowed は出なかった */
button:disabled {
  cursor: not-allowed;
  pointer-events: none;
}

ドラッグしている間だけ形を変える

ドラッグで動かせる要素には grab を書き、押している間だけ :active で grabbing に変えます。

css
.card {
  cursor: grab;
}

.card:active {
  cursor: grabbing;
}

カードを動かす処理は JavaScript で書きました。カードの上にマウスを重ね、マウスのボタンを押したままカードを動かす操作を2回行ったところを録画しました。

ドラッグできるカードの上にマウスを重ねると、開いた手のカーソルが出る。マウスボタンを押すと、カーソルが握った手に変わり、下の表示が「ドラッグ 1回目/2回」「マウスボタン:押している」になって、ピンクの文字で目立つ。押したまま動かすとカードが一緒に動き、ボタンを離すと開いた手に戻る。同じ操作を2回目も行う
カードをドラッグしたときのカーソルの変わり方(2回)

押す前は grab の開いた手、押した直後と、押したまま動かしている間は grabbing の握った手でした。動かしている間も、カードは :active に当てはまっていました。ボタンを離すと、開いた手に戻りました。

自分で用意した画像をカーソルにする

画像を使うときは url() で画像を指定し、最後にキーワードを書きます。

css
.canvas {
  cursor: url(ring.svg) 16 16, crosshair;
}

検証では、32×32px の SVG で、ピンクの輪の中心に点がある画像を作りました。

最後のキーワードを省くと、宣言ごと無視される

MDN の cursor のページにあるとおり、url() を並べた最後には、キーワードを必ず書きます。キーワードを省いて cursor: url(ring.svg); と書くと、CSS.supports() は false を返し、cursor は初期値の auto のままでした。画像は出ず、矢印のカーソルが出ました。画像が出ないときは、まずキーワードを書き忘れていないかを見てください。

url() の後の2つの数値は、画像の中で指す点

url() の後ろの2つの数値は、ホットスポットの座標です。ホットスポットは、画像の中で、実際にポインターが指している点です。画像の左上を 0 0 として、ピクセル単位で書きます。

同じ画像で、ホットスポットを 16 16 にしたときと、書かなかったときを比べました。灰色の十字は、ポインターがある位置です。

2つの画面を縦に並べたもの。上の「url(ring.svg) 16 16, crosshair」では、ピンクの輪の中心が灰色の十字の中心に重なっている。下の「url(ring.svg), crosshair」では、ピンクの輪が灰色の十字の右下にずれていて、輪の左上の角あたりが十字の中心にある
ホットスポットを指定したときと、しないとき(灰色の十字がポインターの位置)

16 16 を書くと、輪の中心がポインターの位置に重なりました。書かないと、X サーバーに渡されたカーソルのホットスポットは 0,0 で、画像の左上がポインターの位置になりました。CSS の仕様では、座標を省いたときは画像ファイルに入っているホットスポットを使い、それもなければ画像の左上を使います。先端が画像の左上の角にある画像なら左上のままでもかまいませんが、輪や十字のように中心で指す画像では、中心の座標を書きます。

大きすぎる画像は使われない

MDN の cursor のページによると、Firefox と Chromium では、カーソルの画像は既定で 128×128px までに制限されています。MDN は、32×32px までにすることを勧めています。

同じ絵を 128×128px と 200×200px の SVG にして試しました。

画像の大きさ 出たカーソル
128×128px 指定した画像(128×128px)
200×200px 画像は使われず、最後のキーワードの crosshair

200×200px のときも、getComputedStyle() の cursor には指定どおりの url() が入っていました。CSS としては正しく読み込まれていても、画像が大きすぎると画面に出るのは代わりのキーワードのカーソルです。

​Posted by 86