3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CSSAdvent Calendar 2024

Day 5

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

Last updated at Posted at 2024-12-03

はじめに

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

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

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

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

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

主な特徴

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

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

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

デザインのポイント

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

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

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

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

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

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

主な機能は

  • ポモドーロタイマー

NeumorphismっぽいUIでポモドーロタイマーを作ってみる_01.jpg

  • ダークモード

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

成果物デモサイト

参考になるサイト

  • uiverse.io

  • neumorphism.io

プロジェクトの作成

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

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

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

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

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を適用する

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);
}

成果物ソース

まとめ

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

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

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

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

関連記事

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?