クリックされた丸角画像に枠を付けるCSS

# はじめに

画像をクリックして選択できる web ページを作成する機会がありました。
選択出来ていることをユーザーに示すため画像に枠線を付けようと考えたのですが、この時に普段バックエンドばかりのエンジニアが苦労して実装した方法を記載します。

画像が丸角なのが悪いよ

# 基本とするページ

本題ではないのでとりあえず画像が選択できる状態まで一気に行きます

```html:index.html
<head>
  <link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
  <input type="radio" id="img_1" name="selectable_image" value="dummy_value_1">
  <label for="img_1">
    <img src="./sample.jpg">
  </label>
  <input type="radio" id="img_2" name="selectable_image" value="dummy_value_2">
  <label for="img_2">
    <img src="./sample.jpg">
  </label>
</body>
```

```css:index.css
img {
  border-radius: 14px;
}
```

![1radio.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315470/0b9bfb38-3583-b2f5-68ff-841d2c6e940b.jpeg)


同じ `neme` のラジオボタンをそれぞれの画像の前において、画像は対応させたいラジオボタンの `id` を `for` に指定した `<label>` に入れています。
これで画像をクリックするとラジオボタンがチェックされます。
複数の画像を選択したい場合はチェックボックスを対応させましょう。

今ラジオボタンを隠すと画像が選択できているか分からなくなるので、選択された画像に視覚的な効果を付けてから非表示にすることにしましょう。


# 画像に枠線を付ける

やりたいことは**選択された**画像に枠線を付けることですが、まずは `<img>` に枠を付けることを目指します。

## 0. `border` を指定する
`<img>` タグには枠線を付ける `border` という属性がありますが色の指定など細かいところに手が届かないし HTML5 で廃止らしいので、css の `border` の方が良いと思います。
まずはこれを使ってみましょう。

後述しますが、今回の需要ではあまり良い方法ではありません。
興味が無い場合この項は斜め読みしましょう。

```html:index.html
<head>
  <link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
  <input type="radio" id="img_1" name="selectable_image" value="dummy_value_1">
  <label for="img_1">
    <img src="./sample.jpg">
  </label>
  <input type="radio" id="img_2" name="selectable_image" value="dummy_value_2">
  <label for="img_2">
    <img src="./sample.jpg">
  </label>
</body>
```

```css:index.css
img {
  border-radius: 14px;
  border: 5px #FF0000 solid;
}
```

![2border.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315470/f3d91757-e81b-e482-5040-52b5ae95f4da.jpeg)


枠線が付きました。


# 選択された要素のみに枠線を付ける

疑似クラスセレクタ `:checked` と隣接セレクタ `+` を使ってチェックされた要素のみに枠線を付けてみましょう。
ついでに枠線が付くようになって選択状態が見えるようになるのでラジオボタンを隠します。

```html:index.html
<head>
  <link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
  <input type="radio" id="img_1" name="selectable_image" value="dummy_value_1">
  <label for="img_1">
    <img src="./sample.jpg">
  </label>
  <input type="radio" id="img_2" name="selectable_image" value="dummy_value_2">
  <label for="img_2">
    <img id="img_2" src="./sample.jpg">
  </label>
</body>
```

```css:index.css
img {
  border-radius: 14px;
}

input[type=radio] {
  display: none;
}

input[type=radio]:checked + label img {
  border: 5px #FF0000 solid;
}
```

![3check.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315470/cbc88d7e-6db6-f5e6-e897-ecb374c30bad.jpeg)


選択すると `border` の分要素のサイズが変わってしまいました。
クリックした瞬間にニュッっと要素が成長する動きは上の静止画で見る以上に違和感がありますし、要素のサイズが変わることでページ全体のレイアウトに影響を与えるかもしれません。

今回はこの方法は避けましょう。


# (要素のサイズを変えずに)画像に枠線を付ける

1. 外側に要素を置いて、そちらに border をかける
1. box-shadow を使う

概ね上記の2手法をネット上では見かけました。
順にやってみます。


## 1. 親要素に枠線を付ける

`<div>` に入れてみます。
`<div>` に入れても横に並んでもらうために全体がさらに `flex` な `<div>` に入れられたり、その都合でこの後の画像では `<img>` 間のスペースが消失していますがあまり気にしないで行きましょう。

1. `<img>` の親要素の `<div class="container">` に `border` を指定している
1. その分子要素の `<img>` にマイナス `margin` をかける
1. ボーダーが裏回るので `<div class="container">` を `overflow: hidden;`

辺りが勘所です。

```html:index.html
<head>
  <link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
  <div id="media_area">
    <input type="radio" id="img_1" name="selectable_image" value="dummy_value_1">
    <label for="img_1">
      <div class="container"><img src="./sample.jpg"></div>
    </label>
    <input type="radio" id="img_2" name="selectable_image" value="dummy_value_2">
    <label for="img_2">
      <div class="container"><img id="img_2" src="./sample.jpg"></div>
    </label>
  </div>
</body>
```

```css:index.css
#media_area {
  display: flex;
}

div.container {
  overflow: hidden;
}

img {
  border-radius: 14px;
}

input[type=radio] {
  display: none;
}

input[type=radio]:checked + label div {
  border: 5px #FF0000 solid;
}

input[type=radio]:checked + label div img {
  margin: -5px;
}
```

![4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315470/269174a2-59d1-3f21-f384-c58a029ee52d.jpeg)

さて無事に要素のサイズが変わらないまま画像に枠線が付きました。

嘘です。無事ではないですね。
角が立ってしまっていますし、わかりにくいですが `border` を `<img>` の上にかぶせて表示した分、画像を覆い隠してしまっています。

お客さんに入稿してもらった画像を表示するサービスだったので、「端っこ隠れちゃう。ゴメンね♡」とは言いたくありません。


## 2. box-shadow

じゃあ今度はこっち。
`<div>` に入れておく必要は無いはずだが、せっかく `flex` にしたのでそのまま。
むしろ `<input type="radio">`, `<label>`, `<img>` は１塊の要素なのでまとめて `<div class="container">` に放り込んでみました。



```html:index.html
<head>
  <link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
  <div id="media_area">
    <div class="container">
      <input type="radio" id="img_1" name="selectable_image" value="dummy_value_1">
      <label for="img_1">
        <img src="./sample.jpg">
      </label>
    </div>
    <div class="container">
      <input type="radio" id="img_2" name="selectable_image" value="dummy_value_2">
      <label for="img_2">
        <img id="img_2" src="./sample.jpg">
      </label>
    </div>
  </div>
</body>
```

```css:index.css
#media_area {
  display: flex;
}

img {
  border-radius: 14px;
}

input[type=radio] {
  display: none;
}

input[type=radio]:checked + label img {
  box-shadow: 0 0 0 5px rgb(255, 0, 0);
}
```

`box-shadow` の各値を簡単に説明すると左から順番に

1. `<offset-x>`: 影の横方向位置、正の値で右に現れる。
1. `<offset-y>`: 影の縦報告位置、正の値で下に現れる。
1. `<blur-radius>`: 影のぼかしの強度。負は無い。
1. `<spread-radius>`: 影の拡散、正の値で要素より大きな影ができる。
1. `<color>`: 影の色。`<color>` の仕様に従う。

つまりこれは要素の真裏にぼかしの無い赤い影を置き、5px だけ周囲に拡散させています。

結果が以下

![5_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315470/91a60b59-d061-852f-eb5d-a8ebf5fe2904.jpeg)

枠線も丸く、大きくなっているように見えますが影は要素のサイズとは別計算のようで画像もその要素も同じサイズのままです。

ただ、これは隣の要素に干渉するようでこんなことになります。

![5_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315470/a651db78-540a-e275-d5ee-ce6c74357768.jpeg)

### 間を空ける

というわけで画像の間を空け直します。
画像の境界もわかりにくくなっていたのでちょうどいいですね。

`<div class="container">` に `padding` を指定するだけです。
要素の間に 5px 欲しいので半分の 2.5px

```html:index.html
<head>
  <link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
  <div id="media_area">
    <div class="container">
      <input type="radio" id="img_1" name="selectable_image" value="dummy_value_1">
      <label for="img_1">
        <img src="./sample.jpg">
      </label>
    </div>
    <div class="container">
      <input type="radio" id="img_2" name="selectable_image" value="dummy_value_2">
      <label for="img_2">
        <img id="img_2" src="./sample.jpg">
      </label>
    </div>
  </div>
</body>
```

```css:index.css
#media_area {
  display: flex;
}

div.container {
  padding: 2.5px;
}

img {
  border-radius: 14px;
}

input[type=radio] {
  display: none;
}

input[type=radio]:checked + label img {
  box-shadow: 0 0 0 5px rgb(255, 0, 0);
}
```

![6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315470/9eee243e-d443-d7fe-4172-831f3b8f1388.jpeg)


# さいごに
以上、画像を選択できるようにする方法と画像に枠線を付ける3つの方法でした。

余談ですが
結果的に仕様に適ったので画像と画像の間を空け最後の方法を使用しましたが、
自分自身に被ってもいいので画像と画像はくっつけておきたい、でも角を丸くするという方法は見出せませんでした。

親要素にも角の丸い要素を指定してみたりいろいろやってみたのですが、うまく合わせきれませんでした。
微妙な隙間が空いてしまうんですよね

私と同じような、角の丸い画像を選択して枠を付けたいという状況の場合、間を空けるか、丸角をやめるか選択をしなければならないと思います。

# 追記
勉強会で発表したところ `<img>` 要素のサイズを維持するには「box-sizing: border-box;でうまくいきませんか？」との指摘を受けた。
せっかくなので折を見て調査したい。
なんとなく `border` が画像に被るようにしないとその分画像が小さくなる気配を感じるが。


# 参考資料
[画像サイズを変更せずにCSSのborderで画像に枠をつける方法](https://helog.jp/html-css/img-border/)
[CSSの小ネタ: 画像に枠線をつける際、borderよりもbox-shadowの方がより美しく実装できる](https://coliss.com/articles/build-websites/operation/css/css-image-contrast-options.html)