<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
  <link href="http://code.jquery.com/ui/1.13.1/themes/cupertino/jquery-ui.min.css" rel="stylesheet" />
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script src="https://code.jquery.com/ui/1.13.1/jquery-ui.min.js"></script>
  <link rel="stylesheet" href="style.css" />
  <link rel="stylesheet" href="theme.css" />
  <link rel="stylesheet" href="ripple.css" />

<header>
    <span class="title">
      CSSテーマサンプル
    </span>
  </header>
  <main>
    <div class="theme-selector">
      <div class="theme-auto ripple">
        <span>自動/環境に合わせる</span>
      </div>
      <div class="theme-light ripple">
        <span>ライト</span>
      </div>
      <div class="theme-dark ripple">
        <span>ダーク</span>
      </div>
      <div class="theme-color-pallet">
        <label for="primary-color">プライマリ色</label>
        <input id="primary-color" type="color" title="色"/>
      </div>
    </div>
  </main>
.ripple {
  position: relative;
  display: block;
  overflow: hidden;

  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
}

/* ripple-effect */

.ripple-effect {
  position: absolute;
  display: inline-block;
  border-radius: 100%;
  background: var(--ripple);
  opacity: 0;
  transform: scale(0);
  animation: ripple-effect 600ms cubic-bezier(0, 0, 0, 1.0) forwards;
  pointer-events: none;
}

@keyframes ripple-effect {
  from {
    opacity: 1;
    transform: scale(0);
  }

  to {
    opacity: .2;
    transform: scale(1);
  }
}

html,
body {
  width: 100%;
  height: 100%;
}

body {
  display: grid;

  grid-template-rows: var(--header-height) 1fr;
}

header {
  position: relative;
  z-index: 101;
  display: grid;
  background-color: var(--header-background);
  box-shadow: var(--header-shadow);
  color: var(--header-color);
  text-align: center;
  font-size: large;

  grid-template-columns: auto 1fr auto;
}

header .title {
  vertical-align: middle;
  line-height: var(--header-height);

  grid-column: 2;
}

main {
  overflow-y: scroll;
  height: 100%;
  background-color: var(--main-background);
  color: var(--main-color);
}

div.ripple {
  margin: 1em;
  background: var(--p-color);
  color: var(--p-text);
  padding: .8em .8em;
  border-radius: 5px;
  transition: all 300ms ease-out;
}

div.ripple:hover,
div.ripple:active {
  background: var(--p-highlight);
}

div.ripple span{
  vertical-align: middle;
}

div.theme-color-pallet {
  margin: 1em;
}

:root {
  --header-height: 48px;

  --header-shadow: rgb(0 0 0 / 20%) 0px 2px 4px -1px,
                   rgb(0 0 0 / 14%) 0px 4px 5px 0px,
                   rgb(0 0 0 / 12%) 0px 1px 10px 0px;

  --ripple: white;
}

:root,
:root[theme="light"] {
  --header-background: #4682b4;
  --header-color: #f5f5f5;
  --main-background: #f5f5f5;
  --main-color: #222222;

  --p-color: #4682b4;
  --p-highlight: #86afd0;
  --p-text: #f5f5f5;
}

:root[theme="dark"] {
  --header-background: #444444;
  --header-color: #dddddd;
  --main-background: #333333;
  --main-color: #dddddd;

  --p-color: #274863;
  --p-highlight: #437dad;
  --p-text: #f5f5f5;
}
/**
 * マウスダウンのリップルイベントハンドラ
 * @param {*} e
 */
 function onRipple (e) {
  const target = $(e.currentTarget)
  const targetW = target.innerWidth()
  const targetH = target.innerHeight()
  const x = e.offsetX
  const y = e.offsetY
  const size = Number.parseInt(Math.max(x, y, targetW - x, targetH - y) * 2)
  const w = size
  const h = size

  const effect = $('<span class="ripple-effect"></span>')
    .css({
      left: x - w / 2,
      top: y - h / 2,
      width: size,
      height: size
      })
    .appendTo(this)
  
  const onMouseUp = (e) => {
    effect.remove()
    target.off('mouseup', onMouseUp)
  }

  target.on('mouseup', onMouseUp)
}

/**
 * リップル初期化
 */
function initRipples () {
  $('.ripple')
    .mousedown(onRipple)
}

const ThemeMode = {
  Auto: null,
  Dark: 'dark',
  Light: 'light'
}

let themMode = ThemeMode.Auto

function getBrowserTheme (mediaQueryList) {
  return mediaQueryList.matches ? 'dark' : 'light'
}

function setTheme (mediaQueryList) {
  const theme = themMode ?? getBrowserTheme(mediaQueryList)
  const htmlElement = document.body.parentElement
  htmlElement.setAttribute('theme', theme)

  const style = getComputedStyle(htmlElement)
  const rgb = style.getPropertyValue('--p-color').trim()
  $('#primary-color').val(rgb)
  clearCustomColorPallet()
}

function clearCustomColorPallet() {
  const bodyStyle = document.body.style
  bodyStyle.removeProperty('--p-color')
  bodyStyle.removeProperty('--p-highlight')
}

const mql = window.matchMedia('(prefers-color-scheme: dark)')
mql.addEventListener('change', setTheme)
setTheme(mql)

$(() => {
  initRipples()

  $('.theme-auto').on('click', () => {
    themMode = ThemeMode.Auto
    setTheme(mql)
  })

  $('.theme-light').on('click', () => {
    themMode = ThemeMode.Light
    setTheme(mql)
  })
  
  $('.theme-dark').on('click', () => {
    themMode = ThemeMode.Dark
    setTheme(mql)
  })

  $('#primary-color').on('change', () => {
    const body = document.body
    const computedStyle = getComputedStyle(body)
    const themeRgb = computedStyle.getPropertyValue('--p-color').trim()
    const pickerRgb = $('#primary-color').val()
    console.log(pickerRgb)

    if (themeRgb !== pickerRgb) {
      const r = Number.parseInt(pickerRgb.substring(1, 3), 16)
      const g = Number.parseInt(pickerRgb.substring(3, 5), 16)
      const b = Number.parseInt(pickerRgb.substring(5, 7), 16)
      const { h, s, l } = rgba2hsla({r, g, b})
      body.style.setProperty('--p-color', pickerRgb)
      body.style.setProperty('--p-highlight', `hsl(${h}, ${s}%, ${Math.min(l + 20, 100)}%)`)

    } else {
      clearCustomColorPallet()
    }
  })
})

const rgba2hsla = ({ r, g, b, a }) => {
  console.log(r, g, b)
  const max = Math.max(r, g, b)
  const min = Math.min(r, g, b)
  const hsla = {
    h: 0,
    s: 0,
    l: (max + min) / 2,
    a: a ?? 255
  }
  const diff = max - min

  if (min !== max) {
    if (max === r) {
      hsla.h = 60 * (g - b) / diff
    } else if (max === g) {
      hsla.h = 60 * (b - r) / diff + 120
    } else if (max === b) {
      hsla.h = 60 * (r - g) / diff + 240
    }
  }

  hsla.s = diff / ((hsla.l <= 127) ? (max + min) : 510 - diff)

  if (hsla.h < 0) {
    hsla.h += 360
  }

  hsla.h = Math.round(hsla.h)
  hsla.s = Math.round(hsla.s * 100)
  hsla.l = Math.round((hsla.l * 100 / 255))
  hsla.a = hsla.a / 255

  return hsla
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.