Build a Fully Interactive CSS‑Only Elevator State Machine – No JavaScript Needed

This article walks through creating a pure‑CSS elevator simulator that uses custom properties, :has() selectors, counters, and aria‑live to model a complete state machine with direction indicators, animated transitions, floor display, and full accessibility without any JavaScript.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Build a Fully Interactive CSS‑Only Elevator State Machine – No JavaScript Needed

As a developer fascinated by state machines, I was inspired by articles like “Building a Complete State Machine with HTML Checkboxes and CSS” and set out to create a simpler, more interactive, macro‑free version—a pure‑CSS elevator simulator with direction indicators, transition animations, a counter, and even accessibility features.

Defining State with CSS Variables

The core of the elevator system relies on CSS custom properties to track state. I use @property rules to type‑define values and enable smooth transitions:

@property --current-floor {
  syntax: "<integer>";
  initial-value: 1;
  inherits: true;
}
@property --previous {
  syntax: "<number>";
  initial-value: 1;
  inherits: true;
}
@property --relative-speed {
  syntax: "<number>";
  initial-value: 4;
  inherits: true;
}
@property --direction {
  syntax: "<integer>";
  initial-value: 0;
  inherits: true;
}

These variables let the elevator compare the current floor with the previous one, compute speed, and drive animations.

Simple UI: Radio Buttons to Choose Floors

Radio inputs act as state triggers. Each floor corresponds to a radio input, and the :has() pseudo‑class detects which one is checked:

<input type="radio" id="floor1" checked>
<input type="radio" id="floor2">
<input type="radio" id="floor3">
<input type="radio" id="floor4">
.elevator-system:has(#floor1:checked) {
  --current-floor: 1;
  --previous: var(--current-floor);
}
.elevator-system:has(#floor2:checked) {
  --current-floor: 2;
  --previous: var(--current-floor);
}

This turns the elevator into a state machine where selecting different buttons triggers transitions and calculations.

Simulating Motion with Dynamic Variables

To move the elevator up or down I use transform: translateY(...) together with --current-floor:

.elevator {
  transform: translateY(calc((1 - var(--current-floor)) * var(--floor-height)));
  transition: transform calc(var(--relative-speed) * 1s);
}

The animation duration depends on the number of floors crossed:

--abs: calc(abs(var(--current-floor) - var(--previous)));
--relative-speed: calc(1 + var(--abs));

Arrow Direction Logic

The arrow points up or down based on floor changes:

--direction: clamp(-1, calc(var(--current-floor) - var(--previous)), 1);
.arrow {
  scale: calc(var(--direction) * 2);
  opacity: abs(var(--direction));
  transition: all 0.15s ease-in-out;
}

Simulating “Memory” with Delay

CSS has no native memory, so I delay the update of --previous to simulate it:

.elevator-system {
  transition: --previous calc(var(--delay) * 1s);
  --delay: 1;
}

During the delay --previous lags behind --current-floor, allowing direction and speed calculations; after the delay they sync, giving CSS a form of “memory”.

Floor Display and Unicode Style

The floor number is shown using a CSS counter with Unicode circled digits:

#floor-display:before {
  counter-reset: display var(--current-floor);
  content: counter(display, top-display);
}
@counter-style top-display {
  system: cyclic;
  symbols: "\278A" "\2781" "\2782" "\2783";
  suffix: "";
}

Improving Accessibility with aria-live

Although CSS cannot change DOM text, I use ::before and counter() to let screen readers announce the current floor:

<div class="sr-only" aria-live="polite" id="floor-announcer"></div>
#floor-announcer::before {
  counter-reset: floor var(--current-floor);
  content: "Now on floor " counter(floor);
}
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
}

Practical Use Cases

No‑JS interactive prototypes

Form progress indicators

Game UI state mechanisms

Logic puzzles or educational tools (pure CSS state tracking)

Reducing JS reliance in performance‑ or security‑constrained environments

Conclusion

This small experiment evolved into a full CSS state machine that moves, indicates direction, and announces floors. Modern CSS features like :has(), @property, counters, and calculations let you build responsive, beautiful, and accessible systems without writing a single line of JavaScript.

image.png
image.png
accessibilitystate machineCSSNo JavaScriptElevator
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.