CSS-only puzzle game (no JS!) by me :-).
How to play?
Guide sunlight from the sun ☀️ to the sunflower 🌻 by rotating mirrors.
Click a mirror to rotate it 90°. Find the arrangement that sends the beam all the way to the plant.
Use Easy, Hard, or Random to generate a new puzzle, or
configure a custom grid with the ⚙
button.
Hit Solution to reveal the answer if you're stuck.
Why is this special?
The puzzle UI and logic both are implemented in HTML+CSS only (no JavaScript).
Note: The actual puzzle is in an iframe which does not have JavaScript. The root frame uses JavaScript
to generate the actual puzzle, add sound effects and other interactive features.
How is the mirror rotation implemented in pure CSS?
The animation below should help explain. The initial state is the first column. There is a radio group of four
radio inputs, whose checked state sets the rotation angle of the triangle. At a time, only one label is
clickable, which when clicked, moves the state to the next column.
| Checked |
|
|
|
|
| Rotation |
0° |
90° |
180° |
270° |
| Mirror |
|
|
|
|
| Clickable |
Label 2 |
Label 3 |
Label 4 |
Label 1 |
What is the logic to find valid paths here?
- Model as a directed graph. Each sun is one node, each plant is one node, and each mirror
contributes four nodes (one per orientation). An edge from A to B exists if light can physically
travel from A to B — suns are source-only, plants are sink-only.
- DFS from each sun. Starting from every sun, traverse outward in all four directions.
When a beam hits a mirror, branch into the possible orientations that reflect from that incoming direction
(each mirror orientation produces a different outgoing direction).
- Emit CSS at each step. Every cell the beam passes through generates a CSS rule.
Empty cells get unconditional rules. Mirror cells add a
:checked condition for that mirror's
orientation, and all conditions from earlier mirrors in the chain are carried forward. The result is
a complete set of rules covering every reachable beam path for every combination of mirror states.
How is the lighting of the path implemented in pure CSS?
Every cell in the grid has four invisible light-beam <div>s (one per direction: down, up, right, left),
all set to display: none by default. At puzzle-build time, JavaScript does a depth-first traversal from each sun
through every possible mirror configuration and emits CSS rules that selectively show beams.
- IDs: Sun:
#sun-x-y. Plant: #plant-x-y.
Mirror radio inputs: #t-x-y-s0 through #t-x-y-s3.
Light beams: #l-row-col-dir (dir: 0=down, 1=up, 2=right, 3=left).
- Selector chaining: Since the radio inputs and the grid are siblings in the DOM,
the
~ combinator lets us write rules like
#t-3-6-s0:checked ~ #t-5-6-s2:checked ~ .grid #l-6-5-0 { display: block; }.
Each mirror along the path adds another :checked condition — so a beam only appears when
every mirror in the chain is correctly oriented.
- Half-beams at mirrors: Inside a mirror cell, the incoming and outgoing beams are each
clipped to 50% of the cell (e.g.
align-self: start; height: 50% for a beam entering from the top).
This creates the visual "bounce" at the reflection point without any extra elements.
Worked example
Path: Sun(3,5) → Triangle(3,6) at orientation 0 → Triangle(5,6) at orientation 2 → Plant(5,7).
- The sun emits downward. Light travels through empty cells, each getting a simple rule:
.grid #l-5-3-0 { display: block; } (no conditions needed — sun always shines).
- Light hits mirror
t-3-6. Orientation 0 reflects down→right. Two half-beam rules are generated:
#t-3-6-s0:checked ~ .grid #l-6-3-0 { display: block; align-self: start; height: 50%; } (incoming, top half)
#t-3-6-s0:checked ~ .grid #l-6-3-2 { display: block; justify-self: end; width: 50%; } (outgoing, right half)
- Light continues right through empty cells (full-width beams), each conditioned on
#t-3-6-s0:checked.
- Light hits mirror
t-5-6. Orientation 2 reflects right→down. Now both mirrors must be checked:
#t-3-6-s0:checked ~ #t-5-6-s2:checked ~ .grid #l-6-5-2 { display: block; ... }
#t-3-6-s0:checked ~ #t-5-6-s2:checked ~ .grid #l-6-5-0 { display: block; ... }
- Light reaches Plant(5,7):
#t-3-6-s0:checked ~ #t-5-6-s2:checked ~ .grid #l-7-5-0 { display: block; opacity: 0; }
The beam is invisible, but its display: block triggers win detection.
Can I make my own puzzles?
Yes! Click the ⚙ button to get started.
How could this be improved?
As a proof of concept, this is already an overkill. But, as an actual game, here's what we could improve:
- Optimize output of CSS selectors. Currently they seem to be a bit long
- Hard puzzle generation - the paths are currently selected at random but there are specific ways to place the mirrors to confuse the average player.
- Online multiplayer - first one to solve the puzzle wins.
- Stars out of five based on how many clicks you made on the mirrors before reaching the solution, the lesser the better!
I found a bug
This is a proof of concept, so it may have subtle snags. Feel free to flex your finding on a forum or fire me
an email for a friendly discussion.