【CSS】cursorプロパティ完全ガイド|全カーソル値・カスタムカーソル・pointer論争まで
HTML/CSS
2023.11.19
CSSのcursorプロパティを使えば、マウスカーソルを指マーク(pointer)に変えたり、ドラッグ用の手のひらアイコンにしたり、独自の画像カーソルを設定することもできます。ただし「ボタンに cursor: pointer を付けるべきか」は、Tailwind CSS v4 が方針を変えたことで改めて議論になっているテーマです。
この記事では、すべてのカーソル値を実際にマウスを乗せて確認できるデモ付きで解説。カスタムカーソルの作り方からpointer論争、タッチデバイス対応まで、cursorプロパティの全知識をまとめました。
CSSのcursorプロパティで変更できる全カーソル値の一覧と使い分け、url()を使ったカスタムカーソルの作り方、そして実務で議論になるcursor: pointer をボタンに使うべきか問題まで。実際にマウスを乗せて確認できるインタラクティブなサンプル付きで解説します。
cursorプロパティとは
cursorプロパティは、要素にマウスカーソルを合わせたときのカーソルの形状を指定するCSSプロパティです。
/* キーワード値 */
.pointer { cursor: pointer; }
.grab { cursor: grab; }
/* カスタム画像 + フォールバック */
.custom { cursor: url("cursor.png") 4 12, pointer; }
/* グローバル値 */
.inherit { cursor: inherit; }
ユーザーがカーソルの変化で「ここはクリックできる」「ドラッグできる」「待機中だ」と直感的に操作を理解できるため、UXを大きく左右するプロパティです。
基本構文
cursor: [ url() [x y]? , ]* キーワード値;
- url(): カスタムカーソル画像のパス(省略可)
- x y: ホットスポット座標(クリック判定位置、省略可)
- キーワード値:
pointerやdefaultなどの組み込み値(必須)
カスタム画像を指定する場合でも、画像が読み込めなかった場合のフォールバックとしてキーワード値が必須です。
全カーソル値の一覧(カテゴリ別)
CSS仕様で定義されているカーソル値をカテゴリ別に整理しました。各サンプルエリアにマウスを乗せるとカーソルが変わります。
一般カーソル
基本的なカーソル形状です。
auto
ブラウザ自動判定
default
通常の矢印
none
カーソル非表示
- auto: ブラウザがコンテキストに応じて自動決定(テキスト上なら
text、リンク上ならpointer)
- default: OSの標準カーソル(通常は矢印)
- none: カーソルを非表示にする(マウスストーカー等で使用。濫用禁止)
リンク・状態カーソル
ユーザーの操作状態を視覚的に伝えるカーソルです。
pointer
指マーク
context-menu
メニュー
help
ヘルプ
wait
読み込み中
progress
処理中
- pointer: リンクやクリック可能な要素を示す指マーク
- context-menu: コンテキストメニュー(右クリックメニュー)が利用可能
- help: ヘルプ情報が利用可能(「?」マーク付き)
- wait: 処理中で操作不可(回転アニメーション)
- progress: 処理中だが操作可能(矢印+回転)
waitとprogressの違いは重要です。waitは「操作を受け付けない」、progressは「バックグラウンドで処理中だが操作可能」を意味します。
選択カーソル
テキストやセルを選択するときのカーソルです。
text
テキスト選択
vertical-text
縦書き選択
cell
セル選択
crosshair
十字
- text: テキスト上で自動的に表示されるIビーム
- vertical-text: 縦書きテキスト用のIビーム(横向き)
- cell: スプレッドシートのセル選択(十字)
- crosshair: 精密選択用の十字カーソル(画像編集等)
ドラッグ&ドロップカーソル
要素の移動やドラッグ操作を示すカーソルです。
move
移動
grab
掴める
grabbing
掴んでいる
copy
コピー
alias
ショートカット
no-drop
ドロップ不可
not-allowed
操作不可
- move: 要素を移動可能(4方向矢印)
- grab: 掴める状態(開いた手)
- grabbing: 掴んでいる状態(握った手)
- copy: ドロップでコピー(矢印+「+」)
- alias: ドロップでショートカット作成
- no-drop: ドロップ不可の領域
- not-allowed: 操作が禁止されている
リサイズカーソル
要素やウィンドウのサイズ変更方向を示します。
n-resize
上
e-resize
右
s-resize
下
w-resize
左
ne-resize
右上
nw-resize
左上
se-resize
右下
sw-resize
左下
双方向リサイズのカーソルもあります。
ew-resize
左右
ns-resize
上下
nesw-resize
斜め(/)
nwse-resize
斜め()
- n/e/s/w-resize: 上・右・下・左の単方向リサイズ
- ne/nw/se/sw-resize: 斜め方向のリサイズ
- ew-resize: 左右の双方向リサイズ(列幅調整に最適)
- ns-resize: 上下の双方向リサイズ(行の高さ調整に最適)
- nesw/nwse-resize: 斜めの双方向リサイズ(画像の角リサイズに最適)
その他のリサイズ・スクロールカーソル
テーブルの列幅・行高さの調整や、スクロール操作で使うカーソルです。
col-resize
列リサイズ
row-resize
行リサイズ
all-scroll
全方向スクロール
- col-resize: 列の境界をドラッグして幅を変更(テーブルヘッダーの境界線に最適)
- row-resize: 行の境界をドラッグして高さを変更
- all-scroll: 全方向スクロール可能(マウスホイールクリック時に表示されるカーソル)
ズームカーソル
- zoom-in: 拡大(虫眼鏡+「+」)— 画像ギャラリーやマップの拡大ボタン
- zoom-out: 縮小(虫眼鏡+「-」)— 拡大した画像を元に戻すボタン
cursor: pointer をボタンに使うべきか?
実務で最も議論になるのが「ボタン要素に cursor: pointer を付けるべきか」という問題です。
W3C仕様の定義
The cursor is a pointer that indicates a link.
— CSS Basic User Interface Module Level 4
W3C仕様では、pointerは「リンクを示す」カーソルとして定義されています。つまり、本来は<a>タグ専用のカーソルです。
ブラウザのデフォルト動作
/* ブラウザのデフォルト */
a { cursor: pointer; } /* リンク → 指マーク ✓ */
button { cursor: default; } /* ボタン → 矢印 */
input { cursor: default; } /* フォーム → 矢印 */
ブラウザはリンクのみpointerにしており、<button>のデフォルトはdefault(矢印)です。
Tailwind CSS v4 の方針転換
Tailwind CSS はv3まで<button>にグローバルにcursor: pointerを適用していましたが、v4で撤廃しました。
/* Tailwind CSS v3 (Preflight) */
button, [role="button"] {
cursor: pointer; /* 自動で指マーク */
}
/* Tailwind CSS v4 — この指定が削除された */
v4では、ボタンにpointerが必要なら明示的にcursor-pointerクラスを追加する方針に変わりました。
主要デザインシステムの対応状況
| デザインシステム |
ボタンのcursor |
方針 |
| Google Material Design |
pointer |
クリック可能なすべての要素に指マーク |
| GitHub Primer |
pointer |
ボタン・リンク両方に指マーク |
| IBM Carbon |
pointer |
インタラクティブ要素すべてに指マーク |
| Adobe Spectrum |
default |
W3C仕様に従い、リンクのみ指マーク |
| Apple Human Interface |
default |
macOS準拠、ネイティブと一貫性 |
| デジタル庁デザインシステム |
使い分け |
リンク=pointer、ボタン=default |
実務での推奨
正解は「プロジェクトで統一ルールを設けること」です。以下が実務的な判断基準になります。
/* パターンA: すべてのクリック可能要素にpointer(多くのWebサイトで採用) */
button,
[role="button"],
input[type="submit"],
input[type="button"],
select,
label[for],
.clickable {
cursor: pointer;
}
/* パターンB: リンクのみpointer(W3C仕様準拠) */
/* ブラウザデフォルトのまま。追加CSSは不要 */
一般的なWebサイト・ブログではパターンA(ボタンにもpointer)の方がユーザーの操作性が向上します。Webアプリケーション(管理画面など)ではパターンBも選択肢になります。
カスタムカーソルの作り方(url())
独自の画像をカーソルとして使用できます。
基本構文
/* 画像ファイルを指定 */
.custom-cursor {
cursor: url("/images/cursor.png"), auto;
}
/* 複数のフォールバックを指定 */
.custom-cursor {
cursor: url("cursor.svg"),
url("cursor.png"),
pointer;
}
最後のキーワード値(auto, pointer 等)は必須です。画像が読み込めなかった場合のフォールバックになります。
ホットスポット座標
ホットスポットとは「カーソル画像のどの位置をクリック判定点にするか」を指定する座標です。
/* cursor: url(画像パス) x座標 y座標, フォールバック; */
/* 矢印カーソル → 左上がクリック位置 */
.arrow { cursor: url("arrow.png") 0 0, auto; }
/* 十字カーソル → 中央がクリック位置(32x32画像の場合) */
.crosshair { cursor: url("cross.png") 16 16, crosshair; }
/* 手のひらカーソル → 人差し指の先端 */
.hand { cursor: url("hand.png") 4 0, pointer; }
座標は画像の左上 (0, 0) からのピクセル位置です。指定しない場合、ブラウザは画像の左上をクリック位置として扱います。
カスタムカーソルの制約
| 項目 |
内容 |
| 推奨サイズ |
32x32px(高解像度ディスプレイでは64x64px) |
| 最大サイズ |
128x128px(超えるとブラウザが無視する場合あり) |
| 対応フォーマット |
.cur, .png, .svg, .gif(アニメGIFは静止画になる) |
| SVGカーソル |
viewBoxとwidth/heightの指定が必須 |
| CORSポリシー |
外部ドメインの画像は使用不可(同一オリジン制限) |
SVGカーソルの利点
高解像度ディスプレイ(Retinaなど)では、SVGカーソルがベストプラクティスです。ベクター形式なのでどの解像度でもシャープに表示されます。
/* SVGファイルを外部読み込み */
.svg-cursor {
cursor: url("pen.svg") 2 30, crosshair;
}
/* SVGをインライン(データURI)で指定 */
.inline-svg {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='14' fill='none' stroke='%23e74c3c' stroke-width='3'/%3E%3C/svg%3E") 16 16, crosshair;
}
grab / grabbing でドラッグ操作を表現
ドラッグ可能なUI(スライダー、カルーセル、ソータブルリスト)では、grabとgrabbingの切り替えが重要です。
CSS のみで実装する方法
/* 掴める状態 → 開いた手 */
.draggable {
cursor: grab;
user-select: none; /* ドラッグ中のテキスト選択を防止 */
}
/* 掴んでいる状態 → 握った手 */
.draggable:active {
cursor: grabbing;
}
ここをクリック&ホールドで grabbing に変化
上のサンプルをクリックして押し続けると、カーソルがgrab(開いた手)からgrabbing(握った手)に変わります。
:active の限界と JavaScript での改善
CSSの:activeだけでは、要素の外にドラッグするとカーソルが元に戻ってしまいます。確実に制御するにはJavaScriptが必要です。
const el = document.querySelector(".draggable");
el.addEventListener("mousedown", () => {
el.style.cursor = "grabbing";
document.body.style.cursor = "grabbing"; // 全体のカーソルも変更
});
document.addEventListener("mouseup", () => {
el.style.cursor = "grab";
document.body.style.cursor = ""; // リセット
});
document.body.style.cursorを同時に変更することで、要素外にマウスが移動してもgrabbingカーソルを維持できます。
cursor: not-allowed の落とし穴
cursor: not-allowedを設定しても、実際にクリックを無効化するわけではないという重要な注意点があります。
/* ❌ 悪い例:見た目だけ無効に見えるが、クリックは通る */
.disabled-looking {
cursor: not-allowed;
opacity: 0.5;
}
/* ✅ 良い例:見た目と機能の両方を無効化 */
.truly-disabled {
cursor: not-allowed;
opacity: 0.5;
pointer-events: none; /* クリックイベント自体を無効化 */
}
/* ✅ ボタンの場合:HTML属性も活用 */
/* <button disabled>送信</button> */
button:disabled {
cursor: not-allowed;
opacity: 0.5;
/* pointer-events: auto; を明示しないとカーソル変更が見えない */
}
pointer-events: none の注意
pointer-events: noneを使うと、cursor プロパティ自体も無効になります。「禁止カーソルを表示しつつクリックを無効にしたい」場合は、ラッパー要素を使うテクニックが有効です。
<!-- ラッパーでカーソルを制御、子要素でクリックを無効化 -->
<div style="cursor: not-allowed; display: inline-block;">
<button style="pointer-events: none; opacity: 0.5;">
送信
</button>
</div>
タッチデバイスでの考慮
タッチデバイスではカーソルが存在しないため、cursor プロパティは基本的に無効です。カーソルに依存しないUI設計が必要です。
@media (pointer) でデバイスを判別
/* マウス/トラックパッドがある場合のみカーソルを変更 */
@media (pointer: fine) {
.draggable {
cursor: grab;
}
.draggable:active {
cursor: grabbing;
}
}
/* タッチデバイスでは視覚的フィードバックで代替 */
@media (pointer: coarse) {
.draggable:active {
transform: scale(0.96);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
}
@media (hover) との組み合わせ
/* hover可能なデバイスでのみカーソルを変更 */
@media (hover: hover) and (pointer: fine) {
.card:hover {
cursor: pointer;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
}
}
/* hover不可のデバイスでは :active で代替 */
@media (hover: none) {
.card:active {
background: #f0f0f0;
}
}
メディアクエリの値一覧
| クエリ |
マウス / トラックパッド |
タッチスクリーン |
pointer: fine |
マッチ |
マッチしない |
pointer: coarse |
マッチしない |
マッチ |
hover: hover |
マッチ |
マッチしない |
hover: none |
マッチしない |
マッチ |
cursor: none とマウスストーカー
cursor: noneでカーソルを非表示にし、JavaScriptで独自のカーソル要素を追従させるテクニックです。ポートフォリオサイトやクリエイティブ系サイトでよく使われます。
基本実装
<!-- カスタムカーソル要素 -->
<div id="stalker"></div>
<style>
.stalker-area {
cursor: none;
}
#stalker {
position: fixed;
width: 20px;
height: 20px;
background: rgba(59, 130, 246, 0.5);
border-radius: 50%;
pointer-events: none; /* クリックを透過 */
transform: translate(-50%, -50%);
transition: transform 0.15s ease-out,
width 0.2s, height 0.2s;
z-index: 9999;
}
/* ホバー時にカーソルを拡大 */
#stalker.hover {
width: 40px;
height: 40px;
background: rgba(59, 130, 246, 0.2);
border: 2px solid rgba(59, 130, 246, 0.6);
}
</style>
<script>
const stalker = document.getElementById("stalker");
document.addEventListener("mousemove", (e) => {
// left/top で位置指定し、CSSの translate(-50%, -50%) で中央揃え
stalker.style.left = e.clientX + "px";
stalker.style.top = e.clientY + "px";
});
// リンクやボタン上でカーソルを拡大
document.querySelectorAll("a, button").forEach((el) => {
el.addEventListener("mouseenter", () => stalker.classList.add("hover"));
el.addEventListener("mouseleave", () => stalker.classList.remove("hover"));
});
</script>
requestAnimationFrame で最適化する
mousemoveイベントは1秒間に60回以上発火するため、requestAnimationFrameでスロットリングするのがベストプラクティスです。
const stalker = document.getElementById("stalker");
let mouseX = 0, mouseY = 0;
let rafId = null;
document.addEventListener("mousemove", (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
// 描画フレームに合わせて更新(重複リクエストを防止)
if (!rafId) {
rafId = requestAnimationFrame(() => {
stalker.style.left = mouseX + "px";
stalker.style.top = mouseY + "px";
rafId = null;
});
}
});
実装のポイント
pointer-events: none: カーソル要素がクリックイベントを奪わないようにする
transform: translate(-50%, -50%): 要素の中心をカーソル位置に合わせる
transitionで遅延: 少し遅らせることで「追従する」感覚を演出
requestAnimationFrame: 高頻度イベントを描画フレームに合わせて間引く
- タッチデバイスでは無効化:
@media (pointer: fine)で限定すること
cursor と user-select の組み合わせ
user-selectと組み合わせることで、より直感的なUIを作れます。
/* ドラッグ可能な要素(テキスト選択不可) */
.sortable-item {
cursor: grab;
user-select: none;
}
.sortable-item:active {
cursor: grabbing;
}
/* コピー禁止コンテンツ */
.no-copy {
user-select: none;
cursor: default; /* テキスト選択カーソルを抑制 */
}
/* 選択可能なテキストを明示 */
.selectable {
user-select: all; /* ワンクリックで全選択 */
cursor: text;
}
cursor: grab
+ user-select: none
cursor: default
+ user-select: none
cursor: text
+ user-select: all
JavaScriptでカーソルを動的に変更
ページ全体のカーソルを一時変更(ローディング中など)
// ローディング開始
document.body.style.cursor = "wait";
// API呼び出し
fetch("/api/data")
.then(res => res.json())
.then(data => {
// ローディング終了 → カーソルをリセット
document.body.style.cursor = "";
renderData(data);
})
.catch(() => {
document.body.style.cursor = "";
});
イベントベースで切り替え
const canvas = document.querySelector("canvas");
// ツール別にカーソルを切り替え
function setTool(tool) {
const cursors = {
pen: "crosshair",
eraser: "cell",
move: "move",
zoom: "zoom-in",
select: "default"
};
canvas.style.cursor = cursors[tool] || "default";
}
Tailwind CSS でのカーソル指定
Tailwind CSSには、cursorプロパティに対応するユーティリティクラスが用意されています。
<!-- 基本クラス -->
<button class="cursor-pointer">クリック</button>
<div class="cursor-grab active:cursor-grabbing">ドラッグ</div>
<button class="cursor-not-allowed opacity-50" disabled>無効</button>
<!-- レスポンシブ修飾子 -->
<div class="cursor-default md:cursor-pointer">
モバイルではdefault、PCではpointer
</div>
<!-- カスタムカーソル(任意値) -->
<div class="cursor-[url(hand.cur),_pointer]">
カスタムカーソル
</div>
よく使うTailwindカーソルクラス
| クラス名 |
CSS出力 |
用途 |
cursor-pointer |
cursor: pointer |
クリック可能要素 |
cursor-grab |
cursor: grab |
ドラッグ可能要素 |
cursor-not-allowed |
cursor: not-allowed |
無効化されたUI |
cursor-wait |
cursor: wait |
処理待ち |
cursor-text |
cursor: text |
テキスト入力 |
cursor-zoom-in |
cursor: zoom-in |
拡大可能要素 |
cursor-none |
cursor: none |
カスタムカーソル |
カーソルが効かないときのトラブルシューティング
cursorプロパティを設定しても反映されないケースと対処法です。
1. 親要素の cursor が優先されている
/* 問題:親にcursorが指定されていると子要素に継承される */
.parent { cursor: not-allowed; }
.parent .child { cursor: pointer; } /* 効かない場合がある */
/* 解決:子要素の詳細度を上げるか、!important を使用 */
.parent > .child { cursor: pointer !important; }
2. pointer-events: none が設定されている
/* 問題:pointer-events: none はcursorも無効化する */
.disabled {
pointer-events: none;
cursor: not-allowed; /* 表示されない */
}
/* 解決:ラッパー要素で制御 */
.wrapper { cursor: not-allowed; }
.wrapper .disabled { pointer-events: none; }
3. z-index で別の要素が上に重なっている
/* 問題:透明な要素が上に重なっている */
.overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
z-index: 10;
/* cursor は overlay のものが表示される */
}
/* 解決:DevToolsで要素を確認、不要なoverlayを削除 */
4. カスタムカーソル画像が読み込めない
/* 問題:パスが間違っている or CORSエラー */
.custom { cursor: url("wrong-path.png"), auto; }
/* チェックポイント */
/* 1. DevToolsのNetworkタブで画像の読み込みを確認 */
/* 2. 相対パスはCSSファイルからの相対パス */
/* 3. 外部ドメインの画像は使用不可 */
/* 4. フォールバック値が必須 */
5. iframeの中でカーソルが変わらない
/* iframeの中のcursorは、iframe内のCSSで指定する必要がある */
/* 親ページのCSSからiframe内は制御できない */
アクセシビリティの考慮
カーソル変更は視覚的フィードバックのみであり、以下の点を必ず考慮してください。
重要な原則
- カーソルだけに頼らない: スクリーンリーダーやキーボード操作のユーザーにはカーソルが見えません。ボタンの形状・色・テキストで操作可能性を伝えましょう
- cursor: not-allowed ≠ 無効化: 前述の通り、
disabled属性やaria-disabledを併用してください
- cursor: none は慎重に: マウスストーカーはマウス操作以外のユーザーには機能しないため、ナビゲーションの代替手段を必ず提供してください
- フォーカスインジケーター: カーソル変更と同等のフィードバックを
:focus-visibleでキーボードユーザーにも提供しましょう
/* カーソルとフォーカスの両方でフィードバックを提供 */
.interactive-element {
cursor: pointer;
}
.interactive-element:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* disabled状態は属性とCSSの両方で */
.interactive-element:disabled,
.interactive-element[aria-disabled="true"] {
cursor: not-allowed;
opacity: 0.5;
}
まとめ
| 場面 |
推奨するカーソル |
| リンク |
pointer(ブラウザデフォルト) |
| ボタン |
pointer(Webサイト)/ default(Webアプリ) |
| ドラッグ可能 |
grab → :activeでgrabbing |
| 無効状態 |
not-allowed + disabled属性 |
| 処理待ち(操作不可) |
wait |
| 処理中(操作可能) |
progress |
| 列幅の調整 |
col-resize |
| 画像の拡大 |
zoom-in |
| 独自デザイン |
url() + フォールバック値 |
cursorプロパティは小さなCSSプロパティですが、ユーザー体験に大きな影響を与えます。「この要素は何ができるのか」をカーソルで直感的に伝え、かつカーソルに頼れないユーザーにも同等の情報を提供することが、良いUI設計の基本です。
関連記事