【React/Next.js】Hover で形が変わる通知付きメールアイコンの作り方

# はじめに

友人との共同開発時に使用したホバー時に形が変わるメールアイコンを作成したので、その作り方を紹介します。この方法を覚えれば、メールアイコン以外にもホバー時に変化させることができます。

# 実装

## 準備

Iconには Lucide Icon を使用しました。以下のコマンドでインストールしてください。

```
pnpm install lucide-react
```

Lucide の詳しい使い方は[こちら](https://lucide.dev/guide/packages/lucide-react)をご覧ください。

https://lucide.dev/icons/

## 要件定義

- ホバーするとメールが開くような演出
- 通知の数がわかるように左上に赤丸の数字
- サイズとカウントを Props として渡せるようにする

## コード

```tsx
'use client'

import React, { useState } from 'react';
import { Mail, MailOpen } from 'lucide-react';

interface MailIconProps {
    count?: number;
    size?: number;
}

export const MailIcon = (p: MailIconProps) => {
    const [hovered, setHovered] = useState(false);
    const count = p.count || 0;
    const iconSize = p.size || 24;
    const badgeSize = Math.max(iconSize * 0.45, 16);
    const fontSize = Math.max(iconSize * 0.25, 10);
    const offset = Math.max(iconSize * 0.2, 6);
    return (
        <div>
            <div
                style={{ position: 'relative', display: 'inline-block' }}
                onMouseEnter={() => setHovered(true)}
                onMouseLeave={() => setHovered(false)}
            >
                {hovered ? (
                    <MailOpen size={iconSize} />
                ) : (
                    <Mail size={iconSize} />
                )}
                {count > 0 && (
                    <div
                        style={{
                            position: 'absolute',
                            top: -offset,
                            right: -offset,
                            backgroundColor: '#E74C3C',
                            color: 'white',
                            borderRadius: '50%',
                            padding: '2px',
                            fontSize: `${fontSize}px`,
                            minWidth: `${badgeSize}px`,
                            height: `${badgeSize}px`,
                            lineHeight: `${badgeSize - 4}px`,
                            textAlign: 'center'
                        }}
                    >
                        {count >= 100 ? '99+' : count}
                    </div>
                )}
            </div>
        </div>
    );
};
```

## コードの説明

1. **コンポーネントの入力の定義**

    メール数とアイコンサイズを定義します。`?`はオプショナルといい、入力があってもなくてもいいことを示します。
    
    ```tsx
    interface MailIconProps {
        count?: number;  // メール数
        size?: number;   // アイコンサイズ
    }
    ```
    <br />

2. **状態管理**

    ホバー状態の管理や、カウント・サイズの初期値設定を行います。
    
    ```tsx
    const [hovered, setHovered] = useState(false);  // ホバー状態の管理
    const count = p.count || 0;                     // カウント未指定時は0
    const iconSize = p.size || 24;                  // サイズ未指定時は24px
    ```
    <br />

3. **サイズ計算**

    通知バッジのサイズ計算をします。
    
    ```tsx
    const badgeSize = Math.max(iconSize * 0.45, 16);  // バッジの大きさ（最小16px）
    const fontSize = Math.max(iconSize * 0.25, 10);   // フォントサイズ（最小10px）
    const offset = Math.max(iconSize * 0.2, 6);       // オフセット（最小6px）
    ```
    <br />
4. **アイコンのレンダリング**

    React での hover 管理は `onMouseEnter` がホバーされた時の動作で、`onMouseLeave` はホバーが離れた時の動作を表します。
    
    ```tsx
    <div
        style={{ position: 'relative', display: 'inline-block' }}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
    >
        {hovered ? (
            <MailOpen size={iconSize} />
        ) : (
            <Mail size={iconSize} />
        )}
    ```
    <br />

5. **通知バッジのレンダリング**
    
    メールがある時だけバッジを表示し、100 以上は '99+' と表示してスタイルを崩さないようにしています。
    
    ```tsx
    {count > 0 && (  // カウントが0より大きい時だけバッジを表示
        <div
            style={{
                position: 'absolute',
                top: -offset,         // 上方向にオフセット
                right: -offset,       // 右方向にオフセット
                backgroundColor: '#E74C3C',  // 赤色背景
                color: 'white',       // 白文字
                borderRadius: '50%',  // 円形に
                padding: '4px',
                fontSize: `${fontSize}px`,
                minWidth: `${badgeSize}px`,
                height: `${badgeSize}px`,
                lineHeight: `${badgeSize - 8}px`,  // 縦方向の中央揃えpadding分の8px
                textAlign: 'center'   // 横方向の中央揃え
            }}
        >
            {count >= 100 ? '99+' : count}  // 100以上の場合は'99+'と表示
        </div>
    )}
    ```
    <br />

6. **コンポーネントの使用例**

    ```tsx
    <MailIcon count={5} size={32} />
    ```
    
    また、このようにすると画面遷移のアイコンとして利用できます。
    
    ```tsx
    <a href="/mail">
        <MailIcon count={ notificationNumber } size={ iconSize }/>
    </a>
    ```


## 結果
![画面収録 2024-12-02 15.43.02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3748983/fedcdf8b-e1f2-7a73-8ab6-5a80e6554c69.gif)

99 以上の場合:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3748983/df5f69ca-a481-9294-8433-ab445f238cf1.png)

# まとめ
今回は、ホバー時に形が変わる通知付きのメールアイコンの作り方を紹介しました。細かい点ではありますが、少しでもアニメーションがあると楽しくなりますよね。メールアイコン以外にも、同様の手法が使えると思います。

それでは、みなさんよいエンジニアライフを〜👋