As a developer with a ardour for state machines, I’ve typically discovered myself impressed by articles like “A Full State Machine Made with HTML Checkboxes and CSS.” The ability of pure CSS-driven state machines intrigued me, and I started to surprise: may I create one thing less complicated, extra interactive, and with out using macros? This led to a undertaking the place I constructed an elevator simulation in CSS, full with course indicators, animated transitions, counters, and even accessibility options.
On this article, I’ll stroll you thru how I used trendy CSS options — like customized properties, counters, the :has() pseudo-class, and @property — to construct a totally useful, interactive elevator that is aware of the place it’s, the place it’s headed, and the way lengthy it’ll take to get there. No JavaScript required.
Defining the State with CSS Variables
The spine of this elevator system is using CSS customized properties to trace its state. Beneath, I outline a number of @property guidelines to permit transitions and typed values:
@property --current-floor {
syntax: "";
initial-value: 1;
inherits: true;
}
@property --previous {
syntax: "";
initial-value: 1;
inherits: true;
}
@property --relative-speed {
syntax: "";
initial-value: 4;
inherits: true;
}
@property --direction {
syntax: "";
initial-value: 0;
inherits: true;
}
These variables permit me to match the elevator’s present ground to its earlier one, calculate motion velocity, and drive animations and transitions accordingly.
A daily CSS customized property (--current-floor) is nice for passing values round, however the browser treats all the pieces like a string: it doesn’t know if 5 is a quantity, a colour, or the identify of your cat. And if it doesn’t know, it could possibly’t animate it.
That’s the place @property is available in. By “registering” the variable, I can inform the browser precisely what it’s (, , and many others.), give it a beginning worth, and let it deal with the sleek in-between frames. With out it, my elevator would simply snap from ground to ground, and that’s not the trip expertise I used to be going for.
A Easy UI: Radio Buttons for Flooring
Radio buttons present the state triggers. Every ground corresponds to a radio enter, and I take advantage of :has() to detect which one is chosen:
.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 mix lets the elevator system turn into a state machine, the place deciding on a radio button triggers transitions and calculations.
Movement through Dynamic Variables
To simulate elevator motion, I take advantage of rework: translateY(...) and calculate it with the --current-floor worth:
.elevator {
rework: translateY(calc((1 - var(--current-floor)) * var(--floor-height)));
transition: rework calc(var(--relative-speed) * 1s);
}
The journey length is proportional to what number of flooring the elevator should traverse:
--abs: calc(abs(var(--current-floor) - var(--previous)));
--relative-speed: calc(1 + var(--abs));
Let’s break that down:
--absoffers absolutely the variety of flooring to maneuver.--relative-speedmakes the animation slower when shifting throughout extra flooring.
So, if the elevator jumps from ground 1 to 4, the animation lasts longer than it does going from ground 2 to three. All of that is derived utilizing simply math expressions within the CSS calc() operate.
Figuring out Course and Arrow Conduct
The elevator’s arrow factors up or down primarily based on the change in ground:
--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;
}
Right here’s what’s occurring:
- The
clamp()operate limits the consequence between-1and1. 1means upward motion,-1is downward, and0means stationary.- This result’s used to scale the arrow, flipping it and adjusting its opacity accordingly.
It’s a light-weight strategy to convey directional logic utilizing math and visible cues with no scripting.
Simulating Reminiscence with --delay
CSS doesn’t retailer earlier state natively. I simulate this by delaying updates to the --previous property:
.elevator-system {
transition: --previous calc(var(--delay) * 1s);
--delay: 1;
}
Whereas the delay runs, the --previous worth lags behind the --current-floor. That lets me calculate course and velocity in the course of the animation. As soon as the delay ends, --previous catches up. This delay-based reminiscence trick permits CSS to approximate state transitions usually completed with JavaScript.
Flooring Counters and Unicode Styling
Displaying ground numbers elegantly grew to become a pleasure because of CSS counters:
#floor-display:earlier than {
counter-reset: show var(--current-floor);
content material: counter(show, top-display);
}
I outlined a customized counter model utilizing Unicode circled numbers:
@counter-style top-display {
system: cyclic;
symbols: "278A" "2781" "2782" "2783";
suffix: "";
}
The 278A to 2783 characters correspond to the ➊, ➋, ➌, ➃ symbols and provides a singular, visible allure to the show. The elevator doesn’t simply say “3,” however shows it with typographic aptitude. This strategy is helpful once you wish to transcend uncooked digits and apply symbolic or visible that means utilizing nothing however CSS.

Accessibility with aria-live
Accessibility issues. Whereas CSS can’t change DOM textual content, it could possibly nonetheless replace screenreader-visible content material utilizing ::earlier than and counter().
#floor-announcer::earlier than {
counter-reset: ground var(--current-floor);
content material: "Now on ground " counter(ground);
}
Add a .sr-only class to visually disguise it however expose it to assistive tech:
.sr-only {
place: absolute;
width: 1px;
top: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
This retains the expertise inclusive and aligned with accessibility requirements.
Sensible Purposes of These Methods
This elevator is greater than a toy. It’s a blueprint. Take into account these real-world makes use of:
- Interactive prototypes with out JavaScript
- Progress indicators in varieties utilizing reside state
- Sport UIs with stock or standing mechanics
- Logic puzzles or academic instruments (CSS-only state monitoring!)
- Decreased JavaScript dependencies for efficiency or sandboxed environments
These methods are particularly helpful in static apps or restricted scripting environments (e.g., emails, sure content material administration system widgets).
Closing Ideas
What began as a small experiment changed into a useful CSS state machine that animates, alerts course, and pronounces adjustments, utterly with out JavaScript. Fashionable CSS can do greater than we regularly give it credit score for. With :has(), @property, counters, and a little bit of intelligent math, you'll be able to construct techniques which can be reactive, lovely, and even accessible.
If you happen to check out this system, I’d like to see your take. And when you remix the elevator (perhaps add extra flooring or challenges?), ship it my means!









