本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
アイコン

太陽のアイコンが月に変わり、ページが暗くなる

デモ

操作:クリック・カーソルを重ねる。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

用途
アイコン
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

太陽は、塗った円の芯と、線端の丸い 8 本の光線で描きます。押すと、光線が回りながら芯の中へしまわれ、膨らんだ芯を欠けの円が枠の外から滑り込んで削り、三日月になります。同時にページ全体が暗くなり、もう一度押すと、同じ動きを逆にたどって太陽と明るいページに戻ります。

光線は 0.4 秒で 90° 回しながら 0.4 倍に縮めて消します。芯は半径 4.5 から 8 へ、欠けの円は右上の外から中へ、どちらも 0.1 秒遅れて 0.55 秒の power3.inOut で動かします。形そのものが変わるので、この 2 つは transform ではなく attr で半径と中心の座標を補間します。ページの色は 0.65 秒で入れ替えます。

欠けは、SVG の mask の中に置いた黒い円です。mask の中の図形は、外側の svg に書いた線の指定を受け継ぐので、そのままだと欠けの縁に線幅ぶんの半透明の輪ができて、月の内側がにじみます。mask の中の図形は stroke を「none」にしておきます。

ページの色は、ルートに置いた CSS 変数(地・面・文字・補足・罫線の 5 色)を 1 本のトゥイーンでまとめて補間し、要素はすべて var() で塗ります。GSAP は var() のままでは補間できないので、明るい側と暗い側の値を CSS から読んで渡します。

最初のテーマは、保存した選択か OS の配色の設定で決め、動かさずに合わせます。状態は aria-pressed で伝え、ボタンに重ねると丸の外に薄い輪を出します。欠けの位置と大きさ、光線を回す角度で印象が変わります。ヘッダーのテーマ切り替えや、設定画面の見た目の切り替えに向いています。

コード

HTML
<!-- その場で切り替わるので button。状態は aria-pressed、名前は aria-label が持つ -->
<button class="theme-toggle" type="button" aria-pressed="false" aria-label="ダークモード">
  <svg class="theme-toggle__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
    <defs>
      <!-- 欠けの円を枠の外から滑り込ませて、芯を三日月の形に削る -->
      <mask id="theme-toggle-bite" maskUnits="userSpaceOnUse" x="-8" y="-8" width="40" height="40">
        <!-- mask の中の図形も svg の stroke を受け継ぐ。none にしないと、欠けの縁がにじむ -->
        <rect x="-8" y="-8" width="40" height="40" fill="#fff" stroke="none" />
        <circle class="theme-toggle__bite" cx="28" cy="-4" r="7.5" fill="#000" stroke="none" />
      </mask>
    </defs>
    <path class="theme-toggle__rays" d="M19.6 12h2M17.37 17.37l1.42 1.42M12 19.6v2M6.63 17.37l-1.42 1.42M4.4 12h-2M6.63 6.63l-1.42-1.42M12 4.4v-2M17.37 6.63l1.42-1.42" />
    <circle class="theme-toggle__core" cx="12" cy="12" r="4.5" mask="url(#theme-toggle-bite)" />
  </svg>
</button>

CSS
/* テーマごとの色。JS がこの値を読み、--bg などをまとめて補間する */
:root {
  --light-bg: #f2f1ed;
  --light-surface: #ffffff;
  --light-ink: #262626;
  --light-mute: #6b6b6b;
  --light-line: rgba(38, 38, 38, 0.12);
  --dark-bg: #111111;
  --dark-surface: #262626;
  --dark-ink: #eeece6;
  --dark-mute: #a8a6a0;
  --dark-line: rgba(238, 236, 230, 0.14);
  --bg: var(--light-bg);
  --surface: var(--light-surface);
  --ink: var(--light-ink);
  --mute: var(--light-mute);
  --line: var(--light-line);
}

/* head で先に data-theme を付けておけば、JS が動く前から暗い色で出る(ちらつかない) */
:root[data-theme="dark"] {
  --bg: var(--dark-bg);
  --surface: var(--dark-surface);
  --ink: var(--dark-ink);
  --mute: var(--dark-mute);
  --line: var(--dark-line);
}

/* ページの要素は、すべて var() で塗る */
body {
  background-color: var(--bg);
  color: var(--ink);
}

.theme-toggle {
  --halo: rgba(128, 128, 128, 0.2); /* 重ねたときの輪の色(どちらのテーマでも見える灰) */
  display: inline-grid;
  place-items: center;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 1px solid var(--ink);
  border-radius: 50%;
  background: none;
  color: var(--ink);
  cursor: pointer;
  transition: box-shadow 0.2s ease-out;
  -webkit-appearance: none;
  appearance: none;
}

/* ぼかしの無い影で、丸の外に薄い輪を出す */
.theme-toggle:hover {
  box-shadow: 0 0 0 6px var(--halo);
}

.theme-toggle:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

/* 24px で描いて線は 2px。線端と角は丸 */
.theme-toggle__icon {
  width: 24px;
  height: 24px;
  overflow: visible;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.theme-toggle__core {
  fill: currentColor;
  stroke: none;
}

@media (prefers-reduced-motion: reduce) {
  .theme-toggle {
    transition: none;
  }
}

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const NAMES = ["bg", "surface", "ink", "mute", "line"]; // テーマで入れ替える色(CSS の --light-◯◯ と --dark-◯◯)
const page = document.documentElement;
const styles = getComputedStyle(page);
// GSAP は var(--name) のままでは補間できないので、値にして渡す
const colors = (theme) =>
  Object.fromEntries(NAMES.map((name) => [`--${name}`, styles.getPropertyValue(`--${theme}-${name}`).trim()]));

// ページの色と、すべての切り替えボタンのアイコンを 1 本のタイムラインで動かす。暗くするのも戻すのも play / reverse
const theme = gsap
  .timeline({ paused: true })
  .fromTo(page, colors("light"), { ...colors("dark"), duration: 0.65, ease: "power2.inOut" }, 0);

const buttons = document.querySelectorAll(".theme-toggle");
buttons.forEach((button, i) => {
  const rays = button.querySelector(".theme-toggle__rays");
  const core = button.querySelector(".theme-toggle__core");
  const bite = button.querySelector(".theme-toggle__bite");
  // ページに 2 つ置いても mask の id がぶつからないよう、番号を付け直す
  const mask = button.querySelector("mask");
  mask.id = `${mask.id}-${i}`;
  core.setAttribute("mask", `url(#${mask.id})`);
  theme
    // 光線は回りながら芯へしまわれる(回す中心は viewBox の中央)
    .fromTo(rays, { scale: 1, rotation: 0, opacity: 1, svgOrigin: "12 12" }, { scale: 0.4, rotation: 90, opacity: 0, svgOrigin: "12 12", duration: 0.4, ease: "power2.in" }, 0)
    // 芯が膨らみ、欠けの円が枠の外から滑り込んで削る。形そのものを変えるので、属性を補間する
    .fromTo(core, { attr: { r: 4.5 } }, { attr: { r: 8 }, duration: 0.55, ease: "power3.inOut" }, 0.1)
    .fromTo(bite, { attr: { cx: 28, cy: -4 } }, { attr: { cx: 16.5, cy: 7.5 }, duration: 0.55, ease: "power3.inOut" }, 0.1);
});

// 動きを減らす設定では、ほぼ一瞬で切り替える
if (reduceMotion) theme.timeScale(20);

// 最初のテーマは、保存した選択か OS の設定で決め、動かさずに合わせる
const saved = localStorage.getItem("theme");
let dark = saved ? saved === "dark" : window.matchMedia("(prefers-color-scheme: dark)").matches;
theme.progress(dark ? 1 : 0);

const apply = () => {
  page.dataset.theme = dark ? "dark" : "light";
  buttons.forEach((button) => button.setAttribute("aria-pressed", String(dark)));
};
apply();

buttons.forEach((button) =>
  button.addEventListener("click", () => {
    dark = !dark;
    dark ? theme.play() : theme.reverse();
    apply();
    localStorage.setItem("theme", dark ? "dark" : "light");
  })
);

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。