New Sep 9, 2024

Time Travelling CSS With :target

The Giants All from CSS-Tricks View Time Travelling CSS With :target on css-tricks.com

Checkbox and radio button hacks are the (in)famous trick for creating games using just CSS. But it turns out that other elements based on user input can be hacked and gamified. There are very 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’ve found, though, is that the :target pseudo-class seems relatively unexplored territory in this area of CSS hacking. It’s an underrated powerful CSS feature when you think about it: :target allows us to style anything based on the selected jump link, so 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 decide for yourself.

The stylesheet will sometimes allow the game to end in a draw, so you at least have a smidge of hope.

No need to worry! CSS hasn’t gone Skynet on us yet. Like any CSS hack, the rule of thumb to determine whether a game is possible to implement with CSS is the number of possible game states. I learned that when I was able to create a 4Ă—Sudoku solver but found a 9Ă—9 version pretty darn near impossible. That’s because CSS hacks come down to hiding and showing game states based on selectors that respond to user input.

Tic Tac Toe has 5,478 legal states reachable if X moves first and there’s a famous algorithm that can calculate the optimal move for any legal state. It stands to reason, then, that we can hack together the Tic Tac Toe game completely in CSS.

OK, but how?

In a way, we are not hacking CSS at all, but rather using CSS as the Lord Almighty intended: to hide, show, and animate stuff. 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 linked to the optimal next move for the computer.

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

Each state our HAML generates looks like this in HTML:


<div class="b" id="--OOX----">
  <svg class="o s">
    <circle></circle>
  </svg>

  <a class="s" href="#OXOOX----">
    <div></div>
  </a>

  <svg class="o s">
    <circle class="c"></circle>
  </svg>

  <svg class="o s">
    <circle class="c"></circle>
  </svg>

  <div class="x"></div>

  <a class="s" href="#O-OOXX---">
    <div></div>
  </a>

  <a class="s" href="#O-OOX-X--">
    <div></div>
  </a>

  <a class="s" href="#O-OOX--X-">
    <div></div>
  </a>

  <a class="s" href="#O-OOX---X">
    <div></div>
  </a>
</div>

With a sprinkling of surprisingly straightforward CSS, we will display only the currently selected game state using :target selectors. We’ll also add a .c class to historical computer moves â€” that way, we only trigger the handwriting animation for the computer’s latest move. This gives the illusion that we are only playing on a single gameboard when we are, in reality, 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 an empty square, the :target pseudo-class displays the updated game state(.s), styled so that the computer’s precalculated response makes an animated entrance (.c).

Note the special case when we start the game: We need to display the initial empty grid before the user selects any jump link. There is nothing to style with :target at the start, so we hide the initial state â€” with the:body:has(:target) #--------- selector — once a jump link is selected. Similarly, if you create your experiments using :target you’ll want to present an initial view before the user begins interacting with your page. 

Wrapping up

I won’t go into â€śwhy” we’d want to implement this in CSS instead of what might be an â€śeasier” path with JavaScript. It’s simply fun and educational to push the boundaries of CSS. We could, for example, pull this off with the classic checkbox hack â€” someone did, in fact.

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


Time Travelling CSS With :target originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Scroll to top