ReactMarkDownでimgタグを使用するときの注意点

ReactMarkdown の `img` タグのカスタムコンポーネントでの課題と解決策を書いてみました。

ニッチな内容ですが、公式にもイシューによく上がっているにも関わらずあまり記事がなかったのでまとめてみました。

https://github.com/remarkjs/react-markdown/issues/93

https://github.com/remarkjs/react-markdown/issues/813

## **1. 背景**
`ReactMarkdown` を利用して Markdown を HTML に変換する際、画像（`img` タグ）をカスタマイズしたい場面があります。

この例でいうと、写真にフォーカスすると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621281/b3b7c021-5be9-4951-841f-c68798c82cd5.png)

マウスをかざしたとき、グレーのオーバーレイをさせるときの対応をimgタグに実装したかった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621281/33603a23-3da3-4bd2-a3ed-7bb0547f4162.png)

このときにdivをつかってオーバーレイを実装していました。

しかし、`ReactMarkdown` はデフォルトで `img` を `<p>` タグでラップする仕様があり、これが原因で以下の問題が発生しました。

- **画像の上に `hover` のオーバーレイを表示したいが、`p` の影響を受ける**
- **`::after` や `::before` を使った `hover` のオーバーレイが適用されない**
- **`p` タグ内に `div` を追加すると HTML 構造が壊れる**
- **`p` タグを変更せずに `img` のみをカスタマイズする方法が不明**

この問題を `ReactMarkdown` のカスタムコンポーネントのみで解決する方法を検討しました。

---

## **2. 問題点**
### **① `ReactMarkdown` のデフォルト動作**
`ReactMarkdown` は以下のように、`img` タグを `<p>` の内部に配置します。

```tsx
import ReactMarkdown from 'react-markdown';

const markdown = `![サンプル画像](https://example.com/sample.jpg)`;

<ReactMarkdown>{markdown}</ReactMarkdown>;
```

➡ 生成される HTML
```html
<p>
  <img src="https://example.com/sample.jpg" alt="サンプル画像" />
</p>
```
`p` タグの影響を受けるため、自由に `img` を加工できない問題が発生しました。

公式やイシューを見ると、MDでの写真は段落として表示するため、pタグで囲む思想のようでした。

---

### **② `::before` や `::after` を使った `hover` オーバーレイが機能しない**
通常の `div` なら `::before` を使ってオーバーレイを表示できますが、`img` タグは **空要素** のため、`::before` や `::after` は適用されません。

```tsx
const StyledImage = styled.img`
  position: relative;
  
  &:hover::before {
    content: 'クリックして拡大';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: white;
    font-size: 16px;
    background: rgba(0, 0, 0, 0.5);
    padding: 8px 12px;
    border-radius: 4px;
  }
`;
```
➡ **`img` タグでは `::before` や `::after` が無視されるため、オーバーレイが表示されない**

---

### **③ `div` を追加すると HTML 構造が壊れる**
`img` を `div` でラップすれば `hover` のオーバーレイを実装できますが、`ReactMarkdown` は `p` タグを変更しないため、HTML が壊れてしまいます。

```tsx
const CustomImage = ({ src, alt }: { src?: string; alt?: string }) => {
  return (
    <div>
      <img src={src} alt={alt} />
      <span>クリックして拡大</span>
    </div>
  );
};

<ReactMarkdown components={{ img: CustomImage }}>{markdown}</ReactMarkdown>;
```
➡ **`p` タグの中に `div` が入るため、不正な HTML 構造になる**

---

## **3. 解決策**
### **`box-shadow` を使って `hover` 時にオーバーレイを適用**
`img` 自体に `box-shadow` を適用し、`hover` した際に擬似オーバーレイを表示する方法が最適でした。

```tsx
const StyledImage = styled.img`
  max-width: 100%;
  height: auto;
  display: block;
  cursor: pointer;
  transition: filter 0.3s ease-in-out, box-shadow 0.3s ease-in-out;

  &:hover {
    filter: brightness(50%);
    box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.5);
  }
`;
```

➡ **`filter: brightness(50%)` で画像を暗くし、`box-shadow` でオーバーレイを再現**

---

### **`span` を使って `hover` テキストを表示**
`div` ではなく `span` を使い、`img` の隣にテキストを配置することで、`p` の影響を受けずに `hover` オーバーレイを適用できます。

```tsx
const OverlayText = styled.span`
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: white;
  font-size: 16px;
  font-weight: bold;
  background: rgba(0, 0, 0, 0.5);
  padding: 8px 12px;
  border-radius: 4px;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
  pointer-events: none;
`;

const ImageWrapper = styled.span`
  position: relative;
  display: inline-block;
  cursor: pointer;

  &:hover ${OverlayText} {
    opacity: 1;
  }
`;
```
➡ **`p` の影響を受けず、`img` の `hover` に対応**

---

## **4. まとめ**
### **`ReactMarkdown` の `img` カスタマイズの問題**
- `img` タグが **デフォルトで `p` にラップされる**
- `::before` や `::after` が **`img` では使えない**
- `div` を追加すると **HTML 構造が崩れる**
- `hover` のオーバーレイが **表示されない**

### **解決策**
`box-shadow` と `filter` を利用し、**`img` 単体で `hover` を適用**  
 `span` を利用し、**テキストオーバーレイを `hover` 時に表示**  
`p` タグを変更せず、**HTML の構造を維持**  

この方法を採用することで、**`ReactMarkdown` の `img` タグを `hover` 可能にし、HTML の構造を壊さずにオーバーレイを適用することが可能になった**。