NeumorphismっぽいUIでポモロードタイマーを作ってみた

# はじめに

業務がバックエンド系のこともあり、

デザインとCSSの勉強も含めて

今回はNeumorphismなUIのポモロードタイマーを作ってみる

Neumorphismとは、(AIによる解説より)

```
ニューモフィズム (Neumorphism) は、UIデザインのトレンドの一つです。リアルな物体のような質感や立体感を表現するのが特徴で、ユーザーインターフェースに深みと柔らかい印象を与えます。

主な特徴

* ソフトな立体感: シャドウとハイライトを使い、まるでボタンやカードがUIの背景から押し出されたような、または埋め込まれたような表現をします。

* ミニマルなデザイン: 装飾を抑えたシンプルでクリーンな見た目になります。

* 落ち着いた色使い: 背景と近い色調を用いることで、控えめで洗練された雰囲気を演出します。

デザインのポイント

* 控えめなシャドウとハイライト: 強すぎるシャドウやハイライトは、デザインを古臭く見せてしまうので注意が必要です。

* 適切なコントラスト: 背景と要素の色のコントラストが低すぎると、要素が認識しづらくなります。アクセシビリティにも配慮して、適切なコントラストを保つことが重要です。

* 繊細なグラデーション: 自然な立体感を出すために、繊細なグラデーションが使われます。
```

# 今回作るポモドーロタイマー

こんな感じのアプリを作成する。

(主にボタンなどに適用しているが柔らかな印象でかっこいい🧑‍🎨)

主な機能は

* ポモドーロタイマー

![NeumorphismっぽいUIでポモドーロタイマーを作ってみる_01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/a57085c8-f3eb-8157-78ac-c8237f278241.jpeg)

* ダークモード

![NeumorphismっぽいUIでポモドーロタイマーを作ってみる.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/d0a6c99b-2355-bd56-49ba-aa8864b3f41e.jpeg)

# 成果物デモサイト

https://shisojuice.github.io/react_pomodorotimer/

# 参考になるサイト

* uiverse.io

https://uiverse.io/

* neumorphism.io

https://neumorphism.io/

# プロジェクトの作成

今回の構成は、React(SPA)とCssライブラリは使わずにVanillaのCssで作成する。

* localstorageを使い、Theme切り替え機能を実装

* 25min+5minが3回と25min+25minの1回の計140分のポモドーロタイマーにする

* シンプルなUIを意識してその他はStartStopボタンとResetボタンのみ配置

```javascript:App.jsx
function App() {
  const dark = 'dark';
  const light = 'light';
  const [theme, setTheme] = useState(() => {
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      return savedTheme;
    }
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    return prefersDark ? dark : light;
  });

  useEffect(() => {
    localStorage.setItem('theme', theme);
    document.body.classList.remove(light, dark);
    document.body.classList.add(theme);
  }, [theme]);

  const toggleTheme = (newTheme) => {
    setTheme(newTheme);
  };

  const longtime = 25 * 60;
  const shorttime = 5 * 60;
  const [timeIndex, setTimeIndex] = useState(0);
  const timecycle = [longtime, shorttime, longtime, shorttime, longtime, shorttime, longtime, longtime]; // (25+5)*3 + (25+25)
  const [timeLeft, setTimeLeft] = useState(timecycle[timeIndex]);
  const minutes = Math.floor(timeLeft / 60);
  const seconds = timeLeft % 60;
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    let interval = null;
    if (isActive && timeLeft > 0) {
      interval = setInterval(() => {
        setTimeLeft(timeLeft => timeLeft - 1);
      }, 1000);
    } else if (!isActive && timeLeft !== 0) {
      clearInterval(interval);
    }
    else if (!isActive && timeLeft == 0) {
      setTimeLeft(timecycle[timeIndex]);
      clearInterval(interval);
    }
    return () => clearInterval(interval);
  }, [isActive, timeLeft, timeIndex]);

  const handleStart = () => {
    setIsActive(true);
  };

  const handleStop = () => {
    setIsActive(false);
    if (minutes == 0 && seconds == 0) {
      setTimeIndex(x => x + 1 > timecycle.length - 1 ? 0 : x + 1);
    }
  };

  const handleReset = () => {
    setTimeLeft(timecycle[timeIndex]);
    setIsActive(false);
  };

  return (
    <>
      <header>
        <button className='themebtn' onClick={() => { toggleTheme(theme == dark ? light : dark); }}>🌞/🌛</button>
      </header>
      <main>
        <div className='timer'>{minutes == 0 && seconds == 0 ? `Finish.` : `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`}</div>
        <div className="cycle_expain">
          <div className='count'>
            {timecycle.map((item, index) => (
              <div
                key={index}
                className={timeIndex == index ? (timeIndex % 2 == 1 ? 'timeindex_break' : 'timeindex_task') : 'timecycle'}
                style={{ width: `${item / 60}px` }}
              ></div>
            ))}
          </div>
          <br />
          <span>{timeIndex % 2 == 1 ? `Break ${timecycle[timeIndex] / 60}min.` : `Task ${timecycle[timeIndex] / 60}min.`}</span>
          <br />
          <span>1 cycle is 140min.</span>
        </div>
        {isActive ?
          (<button onClick={() => { handleStop(); }}>STOP</button>)
          : (<button onClick={() => { handleStart(); }}>START</button>)}
        <button onClick={() => { handleReset(); }}>RESET</button>
      </main>
      <footer>
        <span> © 2024 <a href="https://github.com/shisojuice" target="_blank" rel="noopener noreferrer" _mstmutation="1">shisojuice</a> Pomodoro Timer. All rights reserved.</span>
      </footer>
    </>
  )
}
```
上記の紹介したneumorphism.ioを参考にしつつ、

Neumorphismの柔らかいデザインになるように

shadowとlinear-gradientを意識してCSSを適用する

```css:App.css
:root {
    --text-color: #090909;
    --background-color: #e8e8e8;
    --text-shadow: 6px 6px 12px #c5c5c5, -6px -6px 12px #ffffff;
    --timer-color: #3ed83e;
    --timecycle-box-shadow: inset 3px 3px 0px #d1d1d1, inset -3px -3px 0px #ffffff;
    --timeindex_task-background: linear-gradient(145deg, #f58989, #ce7373);
    --timeindex_task-box-shadow: 2px 2px 7px #ce7373, -1px -1px 7px #ff9393;
    --timeindex_break-background: linear-gradient(145deg, #80b5ec, #6c98c7);
    --timeindex_break-box-shadow: 2px 2px 7px #6c98c7, -1px -1px 7px #84baf3;
    --button-active-box-shadow: inset 4px 4px 12px #c5c5c5, inset -4px -4px 12px #ffffff;
    --anchor-color: #3c4bf4;
    color: var(--text-color);
    background-color: var(--background-color);
    text-shadow: var(--text-shadow);
}

.dark {
    --text-color: #fff;
    --background-color: #212121;
    --text-shadow: 6px 6px 12px #000, -6px -6px 12px #2f2f2f;
    --timer-color: #8adc8a;
    --timecycle-box-shadow: inset 3px 3px 0px #1a1a1a, inset -3px -3px 0px #282828;
    --timeindex_task-background: linear-gradient(145deg, #b91c00, #9c1700);
    --timeindex_task-box-shadow: 2px 2px 7px #9c1700, -1px -1px 7px #be1d00;
    --timeindex_break-background: linear-gradient(145deg, #0060b9, #00519c);
    --timeindex_break-box-shadow: 2px 2px 7px #00519c, -1px -1px 7px #0063be;
    --button-active-box-shadow: inset 4px 4px 12px #c5c5c5, inset -4px -4px 12px #ffffff;
    --anchor-color: #83b8e2;
    color: var(--text-color);
    background-color: var(--background-color);
    text-shadow: var(--text-shadow);
}

.timer {
    color: var(--timer-color);
    text-shadow: var(--text-shadow);
    background: var(--background-color);
    font-size: 96px;
}

.timecycle {
    height: 20px;
    margin: auto 2px;
    border-radius: 3px;
    background: var(--background-color);
    box-shadow: var(--timecycle-box-shadow);
}

.timeindex_task {
    height: 20px;
    margin: auto 2px;
    border-radius: 3px;
    background: var(--timeindex_task-background);
    box-shadow: var(--timeindex_task-box-shadow);
}

.timeindex_break {
    height: 20px;
    margin: auto 2px;
    border-radius: 3px;
    background: var(--timeindex_break-background);
    box-shadow: var(--timeindex_break-box-shadow);
}

button {
    width: 144px;
    color: var(--text-color);
    padding: 0.7em 1.7em;
    font-size: 18px;
    border-radius: 0.5em;
    background: var(--background-color);
    border: 1px solid var(--background-color);
    transition: all 0.3s;
    box-shadow: var(--text-shadow);
}

button:active {
    color: #666;
    box-shadow: var(--button-active-box-shadow);
}

a {
    color: var(--anchor-color);
}
```


# 成果物ソース

https://github.com/shisojuice/react_pomodorotimer

# まとめ

今回は、Neumorphismなポモロードタイマーを作ってみた。

Neumorphismは、柔らかい印象かつモダンな雰囲気があり、かっこいい😎と思ったが、

コントラストが低くなりがちで視認性、アクセシビリティが悪くなってしまったので、

注意する必要があると思った。🤔

関連記事

https://qiita.com/shisojuice/items/303019cc6d05bca3b20c