close
close

Time Traveling CSS with :target

Time Traveling CSS with :target

Checkbox and radio button hacks are the (in)famous trick to making games using only CSS. But it turns out that other elements can be hacked and gamified based on user input. There are some really cool examples of developers getting creative with CSS games based on the :hover pseudo class, and even other games based on the :valid pseudo class.

What I have discovered however is that the :target pseudo class seems like relatively unexplored territory in this area of ​​CSS hacking. It’s an underrated powerful CSS feature if you think about it: :target allows us to style everything based on the selected jumplinkso we have a primitive version of client-side routing built into the browser! Let’s go mad scientist with it and see where that takes us.

Unbeatable AI in CSS

Did I type those words together? Are we going to hack CSS so hard that we hit the singularity? Try to beat the stylesheet below at Tic Tac Toe and see for yourself.

Sometimes the style sheet can ensure that the match ends in a draw, so that at least you still have a glimmer of hope.

Don’t worry! CSS hasn’t become Skynet yet. As with any CSS hack, the rule of thumb for determining whether a game can be implemented with CSS is the number of possible game states. I learned this when I could create a 4×4 Sudoku solver, but found a 9×9 version nearly impossible. That’s because CSS hacks involve hiding and showing game states based on selectors that respond to user input.

Tic Tac Toe has 5,478 legal states that can be reached if X moves first, and there is a famous algorithm that can calculate the optimal move for each legal state. It stands to reason that we can hack the Tic Tac Toe game entirely in CSS.

Okay, but how?

In a way, we’re not hacking CSS at all, we’re using CSS as the Lord Almighty intended: to hide, show, and animate. The “intelligence” is how the HTML is generated. It’s like a “choose your own adventure” book of every possible state in the Tic Tac Toe multiverse with the empty squares mapped to the optimal next move for the computer.

We generate this using a mutated version of the minimax algorithm implemented in Ruby. And did you know that CodePen supports HAML (which supports Ruby blocks), we can secretly use it as a Ruby playground? Now you know.

Each state our HAML generates looks like this in HTML:


With a touch of surprisingly simple CSS we’ll display only the currently selected game state using :target selectors. We also add a .c class to historical computer moves — that way we only activate the handwriting animation for the computer’s last move. This gives the illusion that we’re only playing on one board, when in reality we’re jumping between different sections of the document.

/* Game's parent container */
.b, body:has(:target) #--------- {
  /* Game states */
  .s {
    display: none;
  }
}

/* Game pieces with :target, elements with href */
:target, #--------- {
  width: 300px;
  height: 300px; /
  left: calc(50vw - 150px);
  top: calc(50vh - 150px);
  background-image: url(/path/to/animated/grid.gif);
  background-repeat:  no-repeat;
  background-size: 100% auto;
  
  /* Display that game state and bring it to the forefront  */
  .s {
    z-index: 1;
    display: inline-block;
  }
  
  /* The player's move */
  .x {
    z-index: 1;
    display: inline-block;
    background-image: url("data:image/svg+xml (...)"); /** shortened for brevity **/ 
    height: 100px;
    width: 100px;
  }
  
  /* The browser's move */
  circle {
    animation-fill-mode: forwards;
    animation-name: draw;
    animation-duration: 1s;
    
    /* Only animate the browser's latest turn */
    &.c {
      animation-play-state: paused;
      animation-delay: -1s;
    }
  }
}

When a jump link is selected by clicking on an empty square, :target The pseudo-class displays the updated game state(s), formatted so that the computer’s pre-computed response produces an animated entry (.c).

Note the special case when we start the game: We need to show the empty starting grid before the user selects a jump link. There is nothing to style with :target at the beginning, so we hide the initial state — with the:body:has(:target) #--------- selector — once a jumplink is selected. If you create your experiments using :target You want to give a first impression before the user even engages with your page.

Complete

I won’t go into “why” we would want to implement this in CSS instead of what might be an “easier” path with JavaScript. It’s just fun and educational to push the boundaries of CSS. For example, we could do this with the classic checkbox hack — someone has done it.

Is there anything interesting about using :target instead? I think so, because:

  • We can sgames in CSS! Bookmark the URL so you can come back to it later whenever you want.
  • There is an option to use the browser’s Back and Forward buttons as game controls. It is possible to undo a move by going Back in time or replay a move by navigating Forward. Imagine that you :target using the checkbox hack to create games with a time travel mechanic in the tradition of Braid.
  • Share your game statuses. There is the potential for Wordle-like bragging rights. If you manage to pull off a win or draw against the unbeatable CSS Tic Tac Toe algorithm, you can show off your achievement to the world by sharing the URL.
  • It’s totally semantic HTML. The checkbox hack requires you to hide checkboxes or radio buttons, so it’s always going to be a bit of a hack and a painful trade-off when it comes to accessibility. This approach is arguably not a hack, since we’re just using jump links and divs and their styling. This can even make it — dare I say it — “easier” to provide a more accessible experience. However, that doesn’t mean it’s immediately accessible.