JavaScript + CSS: 決まった要素の上でだけマウスポインタをSVGのアイコンと置き替える

# はじめに
決まった要素にマウスポインタが重なったときだけ、カーソルをSVGのアイコンに置き替える作例です(サンプル001)。目立つポインタにすれば、プレゼンテーションなどに使えるでしょう。CSSの[`cursor`](https://developer.mozilla.org/docs/Web/CSS/cursor)プロパティでも、ポインタのかたちは変えられます。でも、SVGアイコンは好きなデザインや大きさが選べますし、プラットフォームやブラウザにかかわらず、見た目が同じになるというのは利点です。

しかも今回は、決まった要素の上でだけポインタが置き替わるという縛りが加わりました。これは要素に対するマウスイベントを組み合わせて対応します。つぎのサンプル001が作例です。左カラム(Left column)のうえでだけ、ポインタがSVGアイコンに変わります。

#### サンプル001■JavaScript + CSS: Setting custom SVG pointer to element
<p class="codepen" data-height="360" data-theme-id="light" data-default-tab="js,result" data-user="FumioNonaka" data-slug-hash="YzGprQO" style="height: 360px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="JavaScript + CSS: Setting custom SVG pointer to element">
  <span>See the Pen <a href="https://codepen.io/FumioNonaka/pen/YzGprQO">
  JavaScript + CSS: Setting custom SVG pointer to element</a> by Fumio Nonaka (<a href="https://codepen.io/FumioNonaka">@FumioNonaka</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
この作例のおもなポイントはつぎのとおりです。

* 要素の位置をマウスポインタに追随させる
* ポインタ直下の要素にマウスイベントがさえぎられないようスルーさせる
* 決まった要素にだけ対応するようにマウスイベントを組み合わせて使う
* SVGにCSSで縁取りを加える

前に書いた記事「[JavaScript + CSS: ヘッダを上部に固定してカラムの中身はスクロールさせる](https://qiita.com/FumioNonaka/items/6ded2c747db5556f5217)」の[サンプル001](https://qiita.com/FumioNonaka/items/6ded2c747db5556f5217#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB001javascript--css-fixed-top-header-and-scrolling-column)に手を加えるかたちで進めましょう。

# Font Awesomeの読み込み
SVGアイコンは[Font Awesome](https://fontawesome.com/icons?d=gallery)から使わせてもらいます。[CDNで読み込む](https://fontawesome.com/how-to-use/customizing-wordpress/snippets/setup-cdn-webfont#load-all-styles)のが簡単です。

```html
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.1/css/all.css" />
```


# カスタムポインタを加える
カスタムポインタは、左カラム(`id`属性`left-column`)上で出すことにします。同じ階層に加えた`<div>`要素(`id`属性`custom-pointer`)が、SVGのポインタです。なお、作例のスタイルには[Bootstrap](https://getbootstrap.jp/)を使っています。

```html:body要素
<div class="container-fluid d-flex px-0">
	<div id="custom-pointer">
		<i class="fas fa-hand-pointer fa-lg"></i>
	</div>
	<div id="left-column" class="bg-secondary text-light p-2">

	</div>

</div>
```

カスタムポインタのCSSは`position: fixed;`とすることで、位置を自由に変えられるようにします。`z-index`は手前にし、必要な設定を加えてください。これでまずは、左カラムの左上角にカスタムポインタが表れるはずです(図001)。

```css:style要素
#custom-pointer {
	position: fixed;
	color: #3b5998;
	z-index: 1000;
	font-size: 2rem;
}
```

#### 図001■左カラムに加わったカスタムポインタのSVG
![2012002_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/a9c5a22e-0cad-89ad-ffc0-3f0a8b16e289.png)


# カスタムポインタの位置を動かす
`position`を`fixed`にした要素は、CSSの`left`と`top`プロパティで位置が動かせます。JavaScriptコードで用いるのは[`style`](https://developer.mozilla.org/docs/Web/API/ElementCSSInlineStyle/style)プロパティです。プロパティ値は文字列で、CSSと同じく単位が要ります。たとえば、[`DOMContentLoaded`](https://developer.mozilla.org/docs/Web/API/Document/DOMContentLoaded_event)イベントのリスナーにつぎの3行のコードを加えて、カスタムポインタの位置が変わることを確かめてください(図002)。

```js:script要素
document.addEventListener('DOMContentLoaded', () => {

	const customPointer = document.getElementById('custom-pointer');
	customPointer.style.left = '200px';
	customPointer.style.top = '100px';
});
```

#### 図002■カスタムポインタの位置が動いた
![2012002_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/2dbe35e1-a447-d24e-bfc8-7b5b16137515.png)


要素の上でポインタが動いたことを捉えるのは、[`mousemove`](https://developer.mozilla.org/docs/Web/API/Element/mousemove_event)イベントです。要素に加えたイベントリスナーで、カスタムポインタをマウス座標に動かせばよいでしょう。これでSVGを収めた要素(`customPointer`)がマウスポインタに追随します。

```js:script要素
document.addEventListener('DOMContentLoaded', () => {

	const leftColumn = document.getElementById('left-column');

	const mouseAdjust = { x: -13, y: -7 };
	leftColumn.addEventListener('mousemove', ({ clientX, clientY }) => {
		customPointer.style.left = (clientX + mouseAdjust.x) + 'px';
		customPointer.style.top = (clientY + mouseAdjust.y) + 'px';
	});

});
```


# 手前の要素にマウスイベントをスルーさせる
ここで、不便なことに気づきます。左カラムのスクロールバーが動かせません(図003)。なぜかというと、マウス直下のカスタムポインタがイベントを奪って、親の左カラム要素に伝わらないからです。このあと使うつもりの`mouseenter`や`mouseleave`イベントも、意図どおり渡らなくなります。

#### 図003■左カラムのスクロールバーがドラッグできない
![2012002_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/7803272d-d29b-74c5-8920-6653b0988cc9.png)

イベントを邪魔するなと伝えるCSSの設定が、[`pointer-events: none;`](https://developer.mozilla.org/docs/Web/CSS/pointer-events#Values)です。これで、マウスイベントはカスタムポインタの要素をスルーするので、スクロールバーも操作できるようになります。

```css:style要素
#custom-pointer {

	pointer-events: none;
}
```


# 要素の外ではカスタムポインタを消す
左カラム内のカスタムポインタの動きは、これでよいでしょう。でも、マウスポインタが要素の外に出たとき、その直前の位置にカスタムポインタが取り残されてしまいます。左カラムの外では、SVGアイコンは消すべきです。

要素の上にマウスポインタが重なったか、外に出たかは、それぞれマウスイベント[`mouseenter `](https://developer.mozilla.org/docs/Web/API/Element/mouseenter_event)と[`mouseleave `](https://developer.mozilla.org/docs/Web/API/Element/mouseleave_event)で捉えられます。表示/非表示を切り替えるのは、CSSの`display`プロパティです。

```js:script要素
document.addEventListener('DOMContentLoaded', () => {

	leftColumn.addEventListener('mouseenter', (event) => {
		customPointer.style.display = 'block';
	});
	leftColumn.addEventListener('mouseleave', (event) => {
		customPointer.style.display = 'none';
	});
});
```


# SVGに白い縁取りを加える
カスタムポインタの色を暗めの青にしたため、背景色が濃かったり暗いと見づらくなります。白い縁取りを加えることにしましょう。用いるのは[`text-shadow`](https://developer.mozilla.org/docs/Web/CSS/text-shadow)プロパティです。3つ目の数値がぼけ幅で、0にすればくっきりした影になります。とはいえ、水平または垂直にずらさなければ見えません。

幸い、影は複数の設定をカンマ区切りで重ねることができます。上下左右4つの設定を1pxずつずらして組み合わせれば、くっきりとした縁取りができ上がるのです(図004)。

```css:style要素
#custom-pointer {

	text-shadow: 1px 0 0 white,
		0 1px 0 white,
		-1px 0 0 white,
		0 -1px 0 white;
}
```

#### 図004■カスタムアイコンのSVGに白い縁取りが加わった
![2012002_004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/520e67bd-fc6b-8929-9ee8-65584db10f63.png)


### `filter`プロパティに`drop-shadow()`関数を使う
テキストやSVG以外の画像も含めた要素に、そのかたちに沿った縁を加えたい場合には、[`filter`](https://developer.mozilla.org/ja/docs/Web/CSS/filter)プロパティに[`drop-shadow()`](https://developer.mozilla.org/docs/Web/CSS/filter-function/drop-shadow())関数が使えます(「[box-shadowだけじゃない！CSSでできる色々な影の表現と意外に知らない落とし穴](https://ics.media/entry/200406/)」参照)。3つ目の数値がぼけ幅で、関数は複数定められますので、`text-shadow`と同じように用いることができるのです。ただし、まだ仕様は「[草案（WD）](https://developer.mozilla.org/docs/Web/CSS/filter#Specifications)」であることにご注意ください。


# 仕上げ
左カラムの要素の上でだけマウスポインタをSVGのアイコンと置き替えるというお題からは、標準のマウスポインタは消してしまうのがよいでしょう。その場合のCSSの定めは`cursor: none;`です。

でも、カスタムポインタが本当のカーソルから少し遅れることもありえます。また、サイズを大きくしたので、ホットスポット(クリックなどのマウス操作の基準点)がわかりにくくなりそうです。そこで、細いプラス(+)表示を選びました。

```css:style要素
#left-column {

	cursor: crosshair;
}
```

あとは、ロードしたときのカスタムポインタの表示です。`display`プロパティで非表示にしてしまうと、マウスポインタが左カラム上にあるままロードしたとき、`mouseenter`イベントが起こらず表示されません。かといって、`mousemove`のたびに表示し直すというのも冗長です。

そこで、はじめはカスタムポインタをウィンドウの外に出してしまうことにしました。見かけ上、ロード時にポインタは表示されません。左カラムに`mouseenter`、あるいははじめから要素の上にポインタがあったときも、少しでもマウスを動かしたとき`mousemove`で表れることになります。

```js:style要素
document.addEventListener('DOMContentLoaded', () => {

	customPointer.style.left = '-100px';
	customPointer.style.top = '-100px';
});
```

こうしてでき上がったのが、冒頭の[サンプル001](https://codepen.io/FumioNonaka/pen/YzGprQO)です。具体的なコードの記述や動きについては、こちらでお確かめください。