← Blog
CSSUIAccessibilityInteractive

Interactive UI with Pure CSS — Practical Use of transform, transition, and Custom Properties

8 min read

Go deeper on this topic

Canvas vs DOM & Interactive UI

Canvas vs DOM tradeoffs, CSS interactions, custom physics, and portfolio design

Article 3 of 5 in this series.

View topic page

Use the topic hub as a map, then continue to the next article in the series. New here? Start at the home hub →

Canvas API vs. DOM Decision Criteria

On sakimytocom, rendering is split into two approaches:

Condition Choice Reason
100+ elements Canvas DOM reflows are expensive
Text/focus needed DOM Accessibility
Primarily rotation/scaling DOM + CSS Transform GPU compositing
Pixel-level drawing Canvas Flexibility required

FidgetSpinner, InfiniteKnob, and ToggleSwitch all use DOM + CSS Transform.

Physical Rotation with CSS Transform

FidgetSpinner rotates by flicking with a finger. The rotation angle is computed in JS and applied via CSS transform: rotate():

const [angle, setAngle] = useState(0)
const velocityRef = useRef(0)

// Compute angular velocity from drag interaction
function onPointerMove(e: PointerEvent) {
  const dx = e.clientX - centerX
  const dy = e.clientY - centerY
  const currentAngle = Math.atan2(dy, dx)
  const delta = currentAngle - lastAngle.current
  velocityRef.current = delta * 60 // rad/frame → rad/s
}
.spinner {
  transform: rotate(var(--angle));
  will-change: transform;
}

will-change: transform promotes the element to a compositing layer. This offloads rotation to the GPU without blocking the main thread.

Real-Time Control via CSS Custom Properties

Using CSS Custom Properties is more flexible than writing style.transform directly from JS:

element.style.setProperty('--angle', `\${angle}deg`)
element.style.setProperty('--scale', String(scale))
.knob {
  transform: rotate(var(--angle)) scale(var(--scale));
  transition: transform 0.05s ease-out;
}

Three advantages:

  1. CSS media queries can override the variables
  2. Natural composition with transition
  3. Real-time debugging in DevTools

Infinite Rotation with InfiniteKnob

InfiniteKnob rotates continuously beyond 360 degrees. While CSS rotate() only has visual meaning within the 0-360 range, the internal angle value is accumulated:

const totalAngle = useRef(0)

function onRotate(delta: number) {
  totalAngle.current += delta

  // Normalize display angle to 0-360
  const displayAngle = ((totalAngle.current % 360) + 360) % 360
  element.style.setProperty('--angle', `\${displayAngle}deg`)

  // Compute output value from cumulative angle
  const value = totalAngle.current / 360
  onChange(value)
}

((x % 360) + 360) % 360 is a normalization pattern that handles negative angles correctly.

Decay Animation with CSS Transition

The inertial rotation of FidgetSpinner uses requestAnimationFrame for decay calculation:

function decayLoop() {
  if (Math.abs(velocityRef.current) < 0.01) return

  velocityRef.current *= 0.98 // friction
  setAngle(prev => prev + velocityRef.current)
  requestAnimationFrame(decayLoop)
}

CSS Transition alone cannot express exponential decay. transition excels at linear interpolation from A to B, so physical decay is handled in JS while CSS handles only rendering.

Accessibility of ToggleSwitch

The toggle switch is implemented with <button role="switch">:

<button
  role="switch"
  aria-checked={isOn}
  onClick={() => setIsOn(!isOn)}
  className="toggle-track"
>
  <span className="toggle-thumb" />
</button>
.toggle-track {
  width: 48px;
  height: 24px;
  border-radius: 12px;
  background: var(--track-color);
  transition: background 0.2s;
}

.toggle-thumb {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  transform: translateX(var(--thumb-x));
  transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

role="switch" and aria-checked allow screen readers to announce "on/off." Visual animation and accessibility are not mutually exclusive.

Integration with Tailwind CSS v4

In Tailwind v4, dark mode switching is defined with @custom-variant:

@custom-variant dark (&:where(.dark, .dark *));
@custom-variant light (&:where(.light, .light *));

Interactive components need to maintain visibility in both dark and light modes. Defining theme colors with CSS Custom Properties eliminates flicker during theme switching.

Summary: Tools for CSS Interactive UI

For a systematic approach to CSS design patterns, the books on the tool shelf are excellent references. The combination of grid and custom properties, in particular, greatly enhances layout flexibility.