47 CSS Toggles & Switches15 / 47

Pure CSSMIT licensed

Skeuomorphic Wall Light Switch

A realistic wall-plate rocker switch — beveled plate, screw heads, a paddle that tips in 3D perspective — and when you flip it, the whole room warms up: the section's ambient light brightens via :has(). Skeuomorphism with a functional payoff.

Published

Live Demo
Try it

The code

<section class="tgl-15" aria-label="Skeuomorphic wall light switch">
  <span class="tgl-15__glow" aria-hidden="true"></span>
  <div class="tgl-15__plate">
    <span class="tgl-15__screw tgl-15__screw--top" aria-hidden="true"></span>
    <span class="tgl-15__well">
      <input type="checkbox" id="tgl-15-sw" class="tgl-15__sw" role="switch" aria-label="Room light">
      <label class="tgl-15__paddle" for="tgl-15-sw" aria-hidden="true"></label>
    </span>
    <span class="tgl-15__screw tgl-15__screw--bot" aria-hidden="true"></span>
  </div>
  <p class="tgl-15__cap">Flip it — the room lights up</p>
</section>
.tgl-15,
.tgl-15 *,
.tgl-15 *::before,
.tgl-15 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tgl-15 {
  --dusk: #2b2e38;
  --lamp: #f0e2c4;
  position: relative;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 30px;
  padding: 60px 20px;
  background: var(--dusk);
  transition: background-color .8s;
  overflow: hidden;
}

.tgl-15:has(.tgl-15__sw:checked) {
  background: var(--lamp);
}

.tgl-15__glow {
  position: absolute;
  top: 8%;
  left: 50%;
  width: 420px;
  height: 420px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(circle,rgba(255,214,130,.55),transparent 65%);
  opacity: 0;
  transition: opacity .8s;
  pointer-events: none;
}

.tgl-15:has(.tgl-15__sw:checked) .tgl-15__glow {
  opacity: 1;
}

.tgl-15__plate {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  width: 128px;
  padding: 22px 0;
  border-radius: 12px;
  background: linear-gradient(170deg,#f4f1ea,#d9d4c8);
  box-shadow: inset 0 2px 0 rgba(255,255,255,.9),inset 0 -3px 5px rgba(90,80,60,.35),0 18px 34px -14px rgba(0,0,0,.55);
}

.tgl-15__screw {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%,#e8e4da,#a09a8c);
  box-shadow: inset 0 -1px 2px rgba(60,50,30,.5);
  position: relative;
}

.tgl-15__screw::before {
  content: "";
  position: absolute;
  top: 4.5px;
  left: 1.5px;
  right: 1.5px;
  height: 1.5px;
  background: #6e685c;
  border-radius: 1px;
  transform: rotate(-38deg);
}

.tgl-15__screw--bot::before {
  transform: rotate(24deg);
}

.tgl-15__well {
  display: block;
  width: 58px;
  height: 104px;
  border-radius: 8px;
  background: #c9c3b5;
  box-shadow: inset 0 3px 7px rgba(70,60,40,.55);
  padding: 5px;
  perspective: 300px;
  position: relative;
}

.tgl-15__sw {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

.tgl-15__paddle {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 6px;
  background: linear-gradient(180deg,#cfc9bb,#f6f3ec 55%,#efece3);
  box-shadow: 0 4px 8px rgba(60,50,30,.4),inset 0 1px 0 rgba(255,255,255,.8);
  cursor: pointer;
  transform: rotateX(14deg);
  transform-origin: center;
  transition: transform .22s cubic-bezier(.5,1.4,.6,1),background .22s;
}

.tgl-15__sw:checked ~ .tgl-15__paddle {
  transform: rotateX(-14deg);
  background: linear-gradient(0deg,#cfc9bb,#f6f3ec 55%,#efece3);
}

.tgl-15__sw:focus-visible ~ .tgl-15__paddle {
  outline: 3px solid oklch(0.65 0.16 250);
  outline-offset: 4px;
}

.tgl-15__cap {
  position: relative;
  z-index: 1;
  font-size: 12px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #8b8a94;
  transition: color .8s;
}

.tgl-15:has(.tgl-15__sw:checked) .tgl-15__cap {
  color: #8d7f5e;
}

@media (prefers-reduced-motion: reduce) {
  .tgl-15,
    .tgl-15__glow,
    .tgl-15__paddle,
    .tgl-15__cap {
    transition: none;
  }
}
/* No JavaScript — a perspective-tipped paddle rocks rotateX(14deg → -14deg), and :has(:checked) on the section relights the whole room. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Toggles & Switche from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Skeuomorphic Wall Light Switch
Source: https://codefronts.com/components/css-toggles-switches/skeuomorphic-wall-light-switch/

A realistic wall-plate rocker switch — beveled plate, screw heads, a paddle that tips in 3D perspective — and when you flip it, the whole room warms up: the section's ambient light brightens via :has(). Skeuomorphism with a functional payoff.
## HTML
```html
<section class="tgl-15" aria-label="Skeuomorphic wall light switch">
  <span class="tgl-15__glow" aria-hidden="true"></span>
  <div class="tgl-15__plate">
    <span class="tgl-15__screw tgl-15__screw--top" aria-hidden="true"></span>
    <span class="tgl-15__well">
      <input type="checkbox" id="tgl-15-sw" class="tgl-15__sw" role="switch" aria-label="Room light">
      <label class="tgl-15__paddle" for="tgl-15-sw" aria-hidden="true"></label>
    </span>
    <span class="tgl-15__screw tgl-15__screw--bot" aria-hidden="true"></span>
  </div>
  <p class="tgl-15__cap">Flip it — the room lights up</p>
</section>
```
## CSS
```css
.tgl-15,
.tgl-15 *,
.tgl-15 *::before,
.tgl-15 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tgl-15 {
  --dusk: #2b2e38;
  --lamp: #f0e2c4;
  position: relative;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 30px;
  padding: 60px 20px;
  background: var(--dusk);
  transition: background-color .8s;
  overflow: hidden;
}

.tgl-15:has(.tgl-15__sw:checked) {
  background: var(--lamp);
}

.tgl-15__glow {
  position: absolute;
  top: 8%;
  left: 50%;
  width: 420px;
  height: 420px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(circle,rgba(255,214,130,.55),transparent 65%);
  opacity: 0;
  transition: opacity .8s;
  pointer-events: none;
}

.tgl-15:has(.tgl-15__sw:checked) .tgl-15__glow {
  opacity: 1;
}

.tgl-15__plate {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  width: 128px;
  padding: 22px 0;
  border-radius: 12px;
  background: linear-gradient(170deg,#f4f1ea,#d9d4c8);
  box-shadow: inset 0 2px 0 rgba(255,255,255,.9),inset 0 -3px 5px rgba(90,80,60,.35),0 18px 34px -14px rgba(0,0,0,.55);
}

.tgl-15__screw {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%,#e8e4da,#a09a8c);
  box-shadow: inset 0 -1px 2px rgba(60,50,30,.5);
  position: relative;
}

.tgl-15__screw::before {
  content: "";
  position: absolute;
  top: 4.5px;
  left: 1.5px;
  right: 1.5px;
  height: 1.5px;
  background: #6e685c;
  border-radius: 1px;
  transform: rotate(-38deg);
}

.tgl-15__screw--bot::before {
  transform: rotate(24deg);
}

.tgl-15__well {
  display: block;
  width: 58px;
  height: 104px;
  border-radius: 8px;
  background: #c9c3b5;
  box-shadow: inset 0 3px 7px rgba(70,60,40,.55);
  padding: 5px;
  perspective: 300px;
  position: relative;
}

.tgl-15__sw {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

.tgl-15__paddle {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 6px;
  background: linear-gradient(180deg,#cfc9bb,#f6f3ec 55%,#efece3);
  box-shadow: 0 4px 8px rgba(60,50,30,.4),inset 0 1px 0 rgba(255,255,255,.8);
  cursor: pointer;
  transform: rotateX(14deg);
  transform-origin: center;
  transition: transform .22s cubic-bezier(.5,1.4,.6,1),background .22s;
}

.tgl-15__sw:checked ~ .tgl-15__paddle {
  transform: rotateX(-14deg);
  background: linear-gradient(0deg,#cfc9bb,#f6f3ec 55%,#efece3);
}

.tgl-15__sw:focus-visible ~ .tgl-15__paddle {
  outline: 3px solid oklch(0.65 0.16 250);
  outline-offset: 4px;
}

.tgl-15__cap {
  position: relative;
  z-index: 1;
  font-size: 12px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #8b8a94;
  transition: color .8s;
}

.tgl-15:has(.tgl-15__sw:checked) .tgl-15__cap {
  color: #8d7f5e;
}

@media (prefers-reduced-motion: reduce) {
  .tgl-15,
    .tgl-15__glow,
    .tgl-15__paddle,
    .tgl-15__cap {
    transition: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — a perspective-tipped paddle rocks rotateX(14deg → -14deg), and :has(:checked) on the section relights the whole room. */
```

How this works

The plate is layered gradients: an outer plate with a bevel (light top edge, dark bottom via inset shadows), two ::before/::after screw heads with a slotted line, and a recessed well. The paddle is a label inside a perspective container; unchecked it tips toward you with rotateX(14deg), checked it tips away with rotateX(-14deg), while its gradient flips vertically — the two cues together read unmistakably as a physical rocker.

The payoff: .tgl-15:has(:checked) re-lights the scene — background brightens from dusk to warm lamp-light and a radial glow blooms above the plate. Because the transition animates background-color and opacity only, the drama stays cheap.

Make it yours

  • Retheme the room via --dusk / --lamp.
  • Make the paddle taller/narrower for a European rocker profile.
  • Add a soft click: play a 30ms audio tick on change (3 lines of JS).
  • Swap screws to torx by changing the slot pseudo-element to an asterisk glyph.

Gotchas — read before shipping

  • Perspective must sit on the paddle's PARENT; on the paddle itself the tip angle flattens.
  • Keep the paddle's travel implied by gradient + rotation — actually translating it reads as a slider, not a rocker.
  • Ambient scene changes are decorative: the state signal for AT stays on the checkbox itself.

Browser support

ChromeSafariFirefoxEdge
111+15.4+121+111+

Floor set by :has(), oklch() as this demo is written. The core technique itself goes back further — Chrome 105+.

3D transforms are universal; the room-relight uses :has() (evergreen browsers) and degrades to a plain working switch without it.

Techniques used in this demo

Search CodeFronts

Loading…