How I Built a Pure CSS Crossword Puzzle

Adrian Roworth

CSS Crossword Puzzle

Recently I created a pure CSS crossword puzzle implemented using CSS grid that does not need JavaScript in order to work. It gained heavy interest pretty quickly on CodePen. As of this writing, it has more than 350 hearts and 24,000+ page views!

The great CSS Grid Garden tutorial inspired me to build something with Grid Layout features. I wondered if these features could be put to good use in building a crossword puzzle — then I thought, let’s try to create the whole thing without using JavaScript.

Building the Board/Grid

So, first thing’s first, let’s create the board itself!

I ended up with the following basic structure, with HTML comments included to show what the different sections will accomplish:

<div class="crossword-board-container">

  <div class="crossword-board">

    <!-- input elements go here. Uses CSS Grid as its layout -->

    <div class="crossword-board crossword-board--highlight crossword-board--highlight--across">
      <!-- highlights for valid 'across' answers go here. Uses CSS Grid as its layout -->

    <div class="crossword-board crossword-board--highlight crossword-board--highlight-down">
      <!-- highlights for valid 'down' answers go here. Uses CSS Grid as its layout -->

    <div class="crossword-board crossword-board--labels">
      <!-- row and column number labels go here. Uses CSS Grid as its layout -->

    <div class="crossword-clues">

      <dl class="crossword-clues__list crossword-clues__list--across">
        <!-- clues for all the 'across' words go here -->

      <dl class="crossword-clues__list crossword-clues__list--down">
        <!-- clues for all the 'down' words go here -->




That puts our basic skeleton in place so we can add more elements and start styling things.

Using Form Elements for the Squares

The crossword puzzle I’m creating is a 13×13 grid with 44 blank spaces so I need to create 125 input elements each with its own ID in the format item{row number}-{column number}, i.e. item4-12. Here’s what the grid will look like:

Empty crossword

Each of the inputs will get have a minlength and maxlength of “1” to emulate the behaviour of a crossword puzzle (i.e. one letter per square). Each input will also have the required attribute so that HTML5 form validation will be used. I take advantage of all of these HTML5 attributes using CSS.

Using the General Sibling Selector

The input elements are visually laid out in groups (exactly how a crossword puzzle is). Each group of input elements represents a word in the crossword. If each of the elements in that group is valid (which can be verified using the :valid pseudo selector), then we can use CSS to style an element that appears later in the DOM (using an advanced CSS selector called the general sibling selector) that will indicate that the word is correct.

Due to how sibling selectors work, and how CSS works in general, this element has to appear later in the DOM. CSS can only style elements that are after the currently selected element. It cannot look backwards in the DOM (or up the DOM tree) and style something before the current element (at the moment at least anyway).

This means I can use the :valid pseudo-class to style valid elements:

.input:valid {
  border: 2px solid green;
.input:invalid {
  border: 2px solid red;

See the Pen Valid Pseudo Selector Example by SitePoint (@SitePoint) on CodePen.

To style an element later on in the DOM that is a sibling of another element, I can use the ~ (tilde/general sibling) selector, e.g. A ~ B. This selector will select all elements that match B, that are a sibling of A and appear after A in the DOM. For example:

#input1:valid ~ #input2:valid ~ #input3:valid ~ #input4:valid ~ #input5:valid ~ .valid-message {
  display: block;

With this code, if all these input elements are valid, the valid-message element will be displayed.

See the Pen Using Sibling Selector to Display a Message by SitePoint (@SitePoint) on CodePen.

The general sibling selector is extremely useful here. To make the crossword work, I needed to make sure that everything was laid out in a way that allowed me to take advantage of the general sibling selector.

The finished crossword example is using the above technique, starting at line 285. I’ve separated it out in the code block below:

#item1-1:valid ~ #item1-2:valid ~ #item1-3:valid ~ 
#item1-4:valid ~ #item1-5:valid ~ #item1-6:valid ~ 
.crossword-board--highlight .crossword-board__item-highlight--across-1 {
  opacity: 1;

This part of the CSS ensures that if all these input elements are valid, then the opacity of the .crossword-board__item-highlight--across-1 element will be changed. .crossword-board--highlight is a sibling of all the input elements, and .crossword-board__item-highlight--across-1 is a child of .crossword-board--highlight so it’s selectable with CSS!

Indicating Correct Answers

Each crossword answer (i.e. group of input elements) has a corresponding “correct answer indicator” (.crossword-board__item-highlight--across-{{clue number}}) grid item. These grid items are placed behind the input elements on the z-axis, and are hidden using opacity: 0. When a correct word is entered, then the correct answer indicator grid item is displayed by changing the opacity to 1, as the pseudo-class selector snippet above demonstrates.

Crossword with incomplete word

Crossword with completed word

This technique is repeated for each “word” group of input elements. So this means manually creating each CSS rule for each of the input elements in the word group and then selecting the corresponding correct answer indicator grid item. As you can imagine, this makes the CSS get big fast!

So the logical approach would be to create all the CSS rules that show/hide the correct answer indicator grid items for all the horizontal (across) clue answers. Then you would do the same for the vertical clue answers.

Challenges of the Grid System

If, like me, you are trying to use as little CSS as possible, you will quickly realise that you cannot have overlapping grid areas within the same grid system without having to explicitely declare it. They can only sit next to each other (1 across, and 1 down share a square at the top right of the board and this is not possible when using one CSS grid to layout all the correct answer indicator items).

The solution is to wrap each horizontal (across) correct answer indicator grid item in its own grid system, and each vertical (down) correct answer indicator grid item in another. This way I can still use CSS to select them (using the general sibling selector), and they will not interfere with each other and ruin the layout of the grids.

CSS Grid Layout items act similarly to inline-block elements. This basically means that if you specify two grid items to occupy the same space, then the second item will flow around the first item and appear after it in the grid.

See the Pen Grid Layout Module Example by SitePoint (@SitePoint) on CodePen.

In the above example, the first grid item is seven columns wide and spans from the first column to the seventh column. The second grid item is meant to start at the 4th column and span to the 9th column. CSS grid doesn’t like this so it wraps it to the next row. Even if you specify grid-row: 1/1 in the second item, that will take priority and then move the first grid item to the second row.

As explained above, I avoided this by having multiple grids for horizontal and vertical items. This situation can be avoided by specifying the row and column span for each element, but I used the above method to reduce the amount of CSS, and also to have a more maintainable HTML structure.

Checking for Valid Letter Input

Each input element has a pattern attribute with a regular expression as its value. The regular expression matches an uppercase or lowercase letter for that square:

<input id="item1-1" class="crossword-board__item"
       type="text" minlength="1" maxlength="1"
       pattern="^[sS]{1}$" required value="">

This was not ideal because the answers are in the HTML. I wanted to hide the answers in the CSS, but I could not find a way of doing this successfully. I attempted the following technique:

.input#item1-1[value="S"] {
  /* do something... */

But this won’t work. The attribute selector will select the element based on what is actually inside the HTML, and doesn’t consider live changes. So I had to resort to the :valid pseudo-class approach detailed above, and consequently (and annoyingly) exposing the answers in the HTML itself.

Highlighting the Clues on Hover

All horizontal (across) clues are wrapped in a div, as are the vertical (down) clues. These wrapping div elements are siblings of the input elements in the crossword grid. This is demonstrated in the HTML structure listed above in a previous code block. This makes it easy to select the correct clue(s) depending on which input element is being focused/hovered.

Crossword hover

For this, each input element needs :active, :focus, and :hover styles to highlight the relevant clue by applying a background color when the user interacts with an input element.

#item1-1:active ~ .crossword-clues .crossword-clues__list-item--across-1,
#item1-1:focus ~ .crossword-clues .crossword-clues__list-item--across-1,
#item1-1:hover ~ .crossword-clues .crossword-clues__list-item--across-1 {
  background: #ffff74;

Numbering the Clues

The numbers for the clues are positioned using a CSS Grid pattern. Here’s an example of the HTML, abbreviated:

<div class="crossword-board crossword-board--labels">
  <span id="label-1" class="crossword-board__item-label crossword-board__item-label--1">
  <span class="crossword-board__item-label-text">1</span></span>
  <span id="label-2" class="crossword-board__item-label crossword-board__item-label--2">
  <span class="crossword-board__item-label-text">2</span></span>

  <!-- rest of the items here..... -->


Then the CSS looks something like this:

.crossword-board__item-label--1 {
  grid-column: 1/1;

.crossword-board__item-label--2 {
  grid-column: 4/4;

/* etc... more items here... */

Each number is placed at the start position of its related group of input elements (or word). The number is then made to be the width and height of 1 grid square so that it takes up as little space as possible within the grid. It could take up even less room by implementing CSS Grid differently here, but I opted to do it this way.

The “Check for Valid Squares” Checkbox

At the top of the crossword, you’ll notice a checkbox labelled “Check for valid squares”. This allows the user to check if certain letters are correct, even if a given word is wrong.

Crossword valid squares highlighted

Creating this is rather beautiful as it’s one CSS rule that makes all the valid squares get highlighted. It’s using the checkbox hack to select all valid input elements that are after the checked checkbox in the DOM.

Here is the code:

#checkvaliditems:checked ~ .crossword-board-container .crossword-board__item:valid {
  background: #9aff67;


That covers all the main techniques used in the demo. As an exercise, this shows you how far CSS has come in recent years. There are plenty of features we can get creative with. I for one can’t wait to try and push other new features to the limits!

If you want to mess around with the CSS from this article, I’ve put all the code examples into a CodePen collection. The full working CSS crossword Puzzle can be found here.