CSS + JS Data & tools #controls

Perspective playground in CSS + JavaScript

Sliders feed perspective and rotation straight into CSS, with the resulting declaration shown live.

  • 29 lines of CSS
  • 9 lines of HTML
  • 16 lines of JS
  • No dependencies
  • MIT licensed

How it works

  1. perspective is the distance from your eye to the z=0 plane. Low values (≈150px) distort wildly; high values (≈1200px) look almost flat.
  2. It belongs on the parent of the thing you rotate.
  3. The sliders just write three custom properties; the box’s transform reads them.
  4. Try it: set rotateY to 60° and sweep perspective from one end to the other.

Key techniques

  • perspective
  • rotateX / rotateY
  • range inputs → custom properties

Copy-paste code

HTML 9 lines

<div class="play">
  <div class="view"><div class="box">3D</div></div>

  <label>perspective <input type="range" data-var="p"  data-unit="px"  min="120" max="1200" value="400"></label>
  <label>rotateX     <input type="range" data-var="rx" data-unit="deg" min="-80" max="80"   value="20"></label>
  <label>rotateY     <input type="range" data-var="ry" data-unit="deg" min="-80" max="80"   value="-35"></label>

  <code class="out"></code>
</div>

CSS 29 lines

.play {
  display: grid;
  gap: 10px;
  width: min(360px, 90vw);
  font: 14px ui-monospace, monospace;
}

.view {
  display: grid;
  place-items: center;
  height: 260px;
  perspective: var(--p, 400px);
}

.box {
  display: grid;
  place-items: center;
  width: 150px;
  height: 150px;
  border-radius: 16px;
  font: 900 2.4rem system-ui;
  color: #fff;
  background: linear-gradient(135deg, #ffb547, #ff4d9d);
  transform: rotateX(var(--rx, 20deg)) rotateY(var(--ry, -35deg));
}

label { display: grid; color: #949bc0; }
input { accent-color: #ffb547; }
.out  { color: #2ee6d6; }

JS 16 lines

const root = document.querySelector('.play');
const out = root.querySelector('.out');
const inputs = [...root.querySelectorAll('input[type=range]')];

function update() {
  const v = {};
  for (const input of inputs) {
    v[input.dataset.var] = input.value + input.dataset.unit;
    root.style.setProperty('--' + input.dataset.var, v[input.dataset.var]);
  }
  out.textContent =
    'perspective: ' + v.p + '; transform: rotateX(' + v.rx + ') rotateY(' + v.ry + ');';
}

root.addEventListener('input', update);
update();

Sass source 58 lines

.d-play {
  display: grid;
  grid-template-rows: 1fr auto auto;
  width: 100%;
  height: 100%;
  padding: 10px 14px;

  // Its own perspective container, so the slider value is the only one in play.
  &__view {
    display: grid;
    place-items: center;
    perspective: var(--p, 400px);
  }

  &__box {
    display: grid;
    place-items: center;
    width: 110px;
    height: 110px;
    border-radius: 14px;
    background: linear-gradient(135deg, var(--warm), var(--hot));
    color: #fff;
    font-size: 30px;
    font-weight: 900;
    box-shadow: 0 20px 30px -16px var(--hot);
    transform: rotateX(var(--rx, 20deg)) rotateY(var(--ry, -35deg));
  }

  &__controls {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px;
    width: min(100%, 420px); // centred under the card, like every control row
    margin-inline: auto;

    label {
      display: grid;
      color: var(--muted);
      font-family: var(--mono);
      font-size: 11px;
    }

    input {
      width: 100%;
      accent-color: var(--warm);
    }
  }

  &__out {
    margin-top: 4px;
    overflow: hidden;
    color: var(--accent-2);
    font-family: var(--mono);
    font-size: 10.5px;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.