<label class="switch">
  <input type="checkbox" />
  <canvas></canvas>
</label>
.switch {
    display: block;
    cursor: pointer;
    filter: drop-shadow(0 4px 8px rgba(0, 0, 0, .5)) drop-shadow(0 4px 24px rgba(0, 0, 0, .4));
    input {
        display: none;
    }
    canvas {
        display: block;
        margin: -16px -26px;
    }
}

html {
    box-sizing: border-box;
    -webkit-font-smoothing: antialiased;
}

* {
    box-sizing: inherit;
    &:before,
    &:after {
        box-sizing: inherit;
    }
}

// Center & dribbble
body {
    min-height: 100vh;
    display: flex;
    font-family: 'Inter', Arial;
    justify-content: center;
    align-items: center;
    background: #1F1F22;
    .dribbble {
        position: fixed;
        display: block;
        right: 20px;
        bottom: 20px;
        img {
            display: block;
            height: 28px;
        }
    }
    .twitter {
        position: fixed;
        display: block;
        right: 64px;
        bottom: 14px;
        svg {
            width: 32px;
            height: 32px;
            fill: #1da1f2;
        }
    }
}
View Compiled
const { to } = gsap

const width = 108
const height = 68

const backgroundColor = '#F4F4F8'
const dotColor = '#AAAAB7'
const activeColor = '#36363C'

document.querySelectorAll('.switch').forEach(toggle => {
  let canvas = toggle.querySelector('canvas'),
    input = toggle.querySelector('input'),
    mouseX = 0,
    mouseY = 0,
    renderer = new THREE.WebGLRenderer({
      canvas: canvas,
      context: canvas.getContext('webgl2'),
      antialias: true,
      alpha: true
    });

  canvas.style.width = width
  canvas.style.height = height

  renderer.setSize(width, height)
  renderer.setPixelRatio(window.devicePixelRatio || 1)
  renderer.shadowMap.enabled = true
  renderer.shadowMap.type = THREE.PCFSoftShadowMap

  let scene = new THREE.Scene(),
    camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)

  camera.position.z = 120

  let rectangle = new THREE.Shape()
  roundedRect(rectangle, -36, -20, 72, 40, 20)

  let backgroundShape = new THREE.ExtrudeBufferGeometry(rectangle, {
    curveSegments: 20,
    depth: 2,
    bevelEnabled: true,
    bevelSegments: 20,
    steps: 12,
    bevelSize: 6,
    bevelThickness: 6
  })

  let background = new THREE.Mesh(backgroundShape, new THREE.MeshPhongMaterial({
    color: new THREE.Color(backgroundColor),
    shininess: 40
  }))
  background.receiveShadow = true

  scene.add(background)

  let dotShape = new THREE.SphereGeometry(14, 32, 32)

  let sphere = new THREE.Mesh(dotShape, new THREE.MeshPhongMaterial({
    color: new THREE.Color(dotColor),
    shininess: 10
  }))
  sphere.castShadow = true

  scene.add(sphere)

  dotShape.translate(-16, 0, 24)
  sphere.scale.set(.8, .8, .8)

  scene.add(directionLight(.1, 0, 0, 100))
  scene.add(directionLight(.9, 0, 80, 30))
  scene.add(directionLight(.2, 0, -80, 60))
  scene.add(directionLight(.3, -120, -120, -1))
  scene.add(directionLight(.3, 120, -120, -1))

  scene.add(new THREE.AmbientLight(0x626267))

  renderer.domElement.addEventListener('pointermove', e => {
    mouseX = (e.clientX -  e.target.getBoundingClientRect().left - e.target.offsetWidth / 2) * -.8
    mouseY = (e.clientY -  e.target.getBoundingClientRect().top - e.target.offsetHeight / 2) * -.8
  }, false)

  renderer.domElement.addEventListener('pointerleave', e => {
    mouseX = 0
    mouseY = 0
  }, false)

  renderer.domElement.addEventListener('pointerdown', e => {
    to(background.position, {
      z: -4,
      duration: .15
    })
  })

  renderer.domElement.addEventListener('pointerup', e => {
    to(background.position, {
      z: 0,
      duration: .15
    })
  })

  input.addEventListener('change', e => {
    if(input.checked) {
      to(sphere.scale, {
        x: .9,
        y: .9,
        z: .9,
        duration: .6,
        ease: 'elastic.out(1, .75)'
      })
      to(sphere.position, {
        x: 26,
        z: 4,
        duration: .6,
        ease: 'elastic.out(1, .75)'
      })
      let newColor = new THREE.Color(activeColor)
      to(sphere.material.color, {
        r: newColor.r,
        g: newColor.g,
        b: newColor.b,
        duration: .3
      })
      return
    }
    to(sphere.scale, {
      x: .8,
      y: .8,
      z: .8,
      duration: .6,
      ease: 'elastic.out(1, .75)'
    })
    to(sphere.position, {
      x: 0,
      z: 0,
      duration: .6,
      ease: 'elastic.out(1, .75)'
    })
    let newColor = new THREE.Color(dotColor)
    to(sphere.material.color, {
      r: newColor.r,
      g: newColor.g,
      b: newColor.b,
      duration: .3
    })
  })

  let render = () => {
    requestAnimationFrame(render)

    camera.position.x += (mouseX - camera.position.x) * .25
    camera.position.y += (-mouseY - camera.position.y) * .25

    camera.lookAt(scene.position);

    renderer.render(scene, camera)
  }

  render()

})

function roundedRect(ctx, x, y, width, height, radius) {
  ctx.moveTo(x, y + radius)
  ctx.lineTo(x, y + height - radius)
  ctx.quadraticCurveTo(x, y + height, x + radius, y + height)
  ctx.lineTo(x + width - radius, y + height)
  ctx.quadraticCurveTo(x + width, y + height, x + width, y + height - radius)
  ctx.lineTo(x + width, y + radius)
  ctx.quadraticCurveTo(x + width, y, x + width - radius, y)
  ctx.lineTo(x + radius, y)
  ctx.quadraticCurveTo(x, y, x, y + radius)
}

function directionLight(opacity, x, y, z, color = 0xFFFFFF) {
  let light = new THREE.DirectionalLight(color, opacity)
  light.position.set(x, y, z)
  light.castShadow = true

  let d = 4000
  light.shadow.camera.left = -d
  light.shadow.camera.right = d
  light.shadow.camera.top = d * .25
  light.shadow.camera.bottom = -d

  light.shadow.mapSize.width = 1024
  light.shadow.mapSize.height = 1024

  return light
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/three.js/r123/three.min.js
  2. https://unpkg.co/gsap@3/dist/gsap.min.js