Blog Code
Outline


Home

Sudoku with CSS


Two Number Selector

We will start with a very simple example to see how the selections work. We will have a menu at the top to select the number (either 1 or 2) to add to the grid and then a 2x2 grid where each cell can be a 1 or a 2. Click the cells below and try to figure out how the interaction works.


The HTML element needs lots of radio buttons and then a label for each button. There are two buttons at the top to select the active number. Then there are four buttons (one for each cell) for each number. The ID for each grid button will take the form of "mini-(number)-(row)-(column)". The name for each grid button is of the form "mini-(row)-(column)" so that each cell can only be selected by one number.

We will manually place each cell in the correct location (where the --w variable is just the width and height of each cell), but you could do a better job of placing them with CSS. Each label for the grid will be blank and the content will be added with a psuedo-element in CSS.

<div id="mini" style="position: relative; height: calc(3.3 * var(--w) + 8px);">
    <input type="radio" name="mini" id="mini-1" checked/>
    <input type="radio" name="mini" id="mini-2"/>
    <input type="radio" name="mini-1-1" id="mini-1-1-1"/>
    <input type="radio" name="mini-1-2" id="mini-1-1-2"/>
    <input type="radio" name="mini-2-1" id="mini-1-2-1"/>
    <input type="radio" name="mini-2-2" id="mini-1-2-2"/>
    <input type="radio" name="mini-1-1" id="mini-2-1-1"/>
    <input type="radio" name="mini-1-2" id="mini-2-1-2"/>
    <input type="radio" name="mini-2-1" id="mini-2-2-1"/>
    <input type="radio" name="mini-2-2" id="mini-2-2-2"/>
    <label for="mini-1" style="left: 0; top:0;" class="cell">1</label>
    <label for="mini-2" style="left: var(--w); top:0;" class="cell">2</label>
    <label for="mini-1-1-1" style="left: 0; top:calc(1.2 * var(--w));" class="cell n1"></label>
    <label for="mini-1-1-2" style="left: var(--w); top:calc(1.2 * var(--w));" class="cell n1"></label>
    <label for="mini-1-2-1" style="left: 0rem; top:calc(2.2 * var(--w));" class="cell n1"></label>
    <label for="mini-1-2-2" style="left: var(--w); top:calc(2.2 * var(--w));" class="cell n1"></label>
    <label for="mini-2-1-1" style="left: 0; top:calc(1.2 * var(--w));" class="cell n2"></label>
    <label for="mini-2-1-2" style="left: var(--w); top:calc(1.2 * var(--w));" class="cell n2"></label>
    <label for="mini-2-2-1" style="left: 0rem; top:calc(2.2 * var(--w));" class="cell n2"></label>
    <label for="mini-2-2-2" style="left: var(--w); top:calc(2.2 * var(--w));" class="cell n2"></label>
</div>

Selecting a button at the top will change which buttons are checkable within the grid. The z-index of every label of the corresponding number will be raised to place it on top. We also change the background color at the top to visually indicate which number is active.

Then the CSS will make each cell display the correct number when selected. We can use the "[for=mini-1-1-1]" selector to easily match up with the same input.

#mini-1-1-1:checked ~ label[for=mini-1-1-1]::before {
    content: "1";
}
#mini-1-1-2:checked ~ label[for=mini-1-1-2]::before {
    content: "1";
}
#mini-1-2-1:checked ~ label[for=mini-1-2-1]::before {
    content: "1";
}
#mini-1-2-2:checked ~ label[for=mini-1-2-2]::before {
    content: "1";
}
#mini-2-1-1:checked ~ label[for=mini-2-1-1]::before {
    content: "2";
}
#mini-2-1-2:checked ~ label[for=mini-2-1-2]::before {
    content: "2";
}
#mini-2-2-1:checked ~ label[for=mini-2-2-1]::before {
    content: "2";
}
#mini-2-2-2:checked ~ label[for=mini-2-2-2]::before {
    content: "2";
}
#mini-1:checked ~ .n1 {
    z-index: 3;
}
#mini-1:checked ~ label[for=mini-1] {
    background: #CCF;
}
#mini-2:checked ~ .n2 {
    z-index: 3;
}
#mini-2:checked ~ label[for=mini-2] {
    background: #CCF;
}

Delete Digit

Adding one more radio button will allow the user to delete a previously selected number. These buttons will be on top in cells where a number has already been chosen.


In the HTML, we do exactly what we would do to add a third number. You could set the id to a different format, but it is easy to just add an extra iteration to create the delete buttons. I only use "mini" and "minid"









to keep all of the different versions unique on this page.

<input type="radio" name="minid-1-1" id="minid-3-1-1"/>
<input type="radio" name="minid-1-2" id="minid-3-1-2"/>
<input type="radio" name="minid-2-1" id="minid-3-2-1"/>
<input type="radio" name="minid-2-2" id="minid-3-2-2"/>
<label for="minid-3-1-1" style="left: 0; top:calc(1.2 * var(--w));" class="cell n3"></label>
<label for="minid-3-1-2" style="left: var(--w); top:calc(1.2 * var(--w));" class="cell n3"></label>
<label for="minid-3-2-1" style="left: 0rem; top:calc(2.2 * var(--w));" class="cell n3"></label>
<label for="minid-3-2-2" style="left: var(--w); top:calc(2.2 * var(--w));" class="cell n3"></label>

In the CSS, though, we don't want to add any content to these labels. Instead of pushing them to the top when a menu button is checked, we display the delete label in any cell that already has a number.

input[name=minid-1-1]:Not(#minid-3-1-1):checked ~ label[for=minid-3-1-1] {
    z-index: 5;
}

The inputs with "[name=minid-1-1]" will include the buttons for each possible number in the first row and first column. We don't want to display the delete label if it is the one that has been checked so we use the ":Not(#minid-3-1-1)". You can use classes to make better selectors. Repeat this block of CSS for each row and column.

Nine Numbers

We're going to use a templating system to create the many boxes required to handle a normal 9 digit puzzle. The process is the same but we need 810 inputs and labels. For each of the 81 cells, we need a button for each possible digit and then one for deletion.

To avoid a lot of manual work, I will use nunjucks to automate this process. The logic behind all of the below code is the same as the simple example above but there is just more of everything.

<div id="full" style="position: relative; height: calc(10.3 * (var(--w) + 2px)); display: inline-block;">
{% for i in range(1,10) %}
    <input type="radio" name="mini" id="mini-{{i}}"/>
{% endfor %}
{% for i in range(1,11) %}
    {% for r in range(1,10) %}
        {% for c in range(1,10) %}
            <input type="radio" name="full-{{r}}-{{c}}" id="full-{{i}}-{{r}}-{{c}}"/>
        {% endfor %}
    {% endfor %}
{% endfor %}
{% for i in range(1,10) %}
    <label for="full-{{i}}" style="left: calc({{i - 1}} * var(--w)); top:0;" class="cell">{{i}}</label>
{% endfor %}
{% for i in range(1,11) %}
    {% for r in range(1,10) %}
        {% for c in range(1,10) %}
            <label for="full-{{i}}-{{r}}-{{c}}" style="left: calc({{c - 1}} * var(--w)); top:calc({{ r }}.2 * var(--w));" class="cell n{{i}}"></label>
        {% endfor %}
    {% endfor %}
{% endfor %}
</div>

The CSS is also generated with nunjucks. You can generate a separate CSS file or do it inline for simplicity. Some styling doesn't change and can be separated into a different file.

{% for i in range(1,10) %}
	{% for r in range(1,10) %}
		{% for c in range(1,10) %}
		#full-{{i}}-{{r}}-{{c}}:checked ~ label[for=full-{{i}}-{{r}}-{{c}}]::before {
    		content: "{{i}}";
		}
		{% endfor %}
	{% endfor %}
{% endfor %}
{% for i in range(1,10) %}
#minid-{{i}}:checked ~ .n{{i}} {
    z-index: 3;
}
#minid-{{i}}:checked ~ label[for=minid-{{i}}] {
    background: #CCF;
}
{% endfor %}
{% for r in range(1,10) %}
	{% for c in range(1,10) %}
	input[name=full-{{r}}-{{c}}]:Not(#full-{{10}}-{{r}}-{{c}}):checked ~ label[for=full-{{10}}-{{r}}-{{c}}] {
    	z-index: 5;
	}
	{% endfor %}
{% endfor %}

Puzzle Board

Each 3x3 block needs a thicker border to clearly separate the blocks. To avoid these difficulties we could make the interior borders a different style (like dashed) or a different color. If you have made it this far in the article, though, I'm guessing you don't mind making life more difficult for no real reason.

The inputs need to be placed before the grid in a way that their status can cascade to the grid, but they are not visible so it does not really matter. Fortunately, the labels can be placed anywhere so it is possible to put them inside of nested divs. It is probably better to create sub-elements that contain each block, but I like doing every by row and column and then awkwardly adding the blocks. Creating the correct looping process to make the 3x3 blocks start the nesting might be just as awkward.

I am going to explicitly set the border sizes and location of each row and column. We will assign class names to each cell for the row and column and then iterate over all of these classes. You can look at this repl to see how this is done, but you should try to do something better. Locating and styling the elements should be the easy part of using CSS.

Initial Puzzle

Some of the radio boxes need to be pre-checked. These cells also should be impossible to erase and don't require all of the other buttons to be generated at all.

I will convert a one line puzzle to an object that indicates which numbers to show at the start and which buttons can be ignored because they are not possible. The cells object will include keys that the template can easily use to pre-check the appropriate radio button. The ignores object includes keys that the template uses to make sure those buttons are never added to the HTML or CSS.
var rawPuzzle = "..16.98....6.4.7..9.42.56.13..462..8...........5.9.2.............8.2.1..2.98.35.6";
var puzzle = {cells: {},ignores:{}};
for (var i=0;i<rawPuzzle.length;i++){
	if (rawPuzzle.at(i) != "."){
		var ii =  parseInt(rawPuzzle.at(i)) * 81;
		ii +=  (Math.floor(i/9)+1) * 9;
		ii += ((i%9)+1);
		puzzle.cells[ii]=true;//these buttons need to be pre-checked
		for (var iii=1;iii<11;iii++){
			if (iii == parseInt(rawPuzzle.at(i)) ){continue;}
			var ii =  iii * 81 + (Math.floor(i/9)+1) * 9 + ((i%9)+1);
			puzzle.ignores[ii]=true;//these buttons will never be needed
		}
	}
}

You can generate these puzzle strings from QQWing or use a different format to create your puzzle object. If you want larger puzzles you will need to make some adjustments. Either use an array or convert letters to numbers greater than 9.

Block Illegal Plays

A feature that makes the puzzle easier for the user is to block them from inputting the same digit multiple times in one row, column, or 3x3 block.

If we give each input and label class names indicating the number of that input, the row, the column, and the 3x3 block then we can create selectors to ignore the labels for any cells that would be duplicates. Determining the row and column is easy, but getting the correct block requires some modular arithmetic and division.

The class names that start with "bn-" or "n-" represent the number within that cell. The class names that start with "br-" or "row-" represent the row of that cell. Predictably, the next loops represent the column and block

{% for i in range(1,10) %}
  {% for r in range(1,10) %}
    .bn-{{i}}.br-{{r}}:checked ~ #n{{i}} ~ #game .n-{{i}}.row-{{r}} {
        pointer-events: none;
    }
  {% endfor %}
  {% for c in range(1,10) %}
    .bn-{{i}}.bc-{{c}}:checked ~ #n{{i}} ~ #game .n-{{i}}.col-{{c}} {
        pointer-events: none;
    }
  {% endfor %}
  {% for b in range(1,10) %}
    .bn-{{i}}.bb-{{b}}:checked ~ #n{{i}} ~ #game label.n-{{i}}.blk-{{b}} {
           pointer-events: none;
    }
  {% endfor %}
{% endfor %}

We will also indicate which numbers have been exhausted. Once a number has been inputted 9 times we will slightly gray the background for that number and prevent it from being selected. When that happens we will also hide a well-placed invisible block. When all nine numbers have been exhausted all of those invisible blocks will be gone and thus a visible block will move into place over the game board. Complete a puzzle to see what this block looks like.
{% for i in range(1,10) %}
	{% for ii in range(1,10) %}.bn-{{i}}:checked ~ {% endfor %}#n{{i}} ~ #top > span:nth-child({{i}}) {
		pointer-events: none;
		background: #DDD;
	}
	{% for ii in range(1,10) %}.bn-{{i}}:checked ~ {% endfor %}#win > div > span:nth-child({{i}}) {
		display: none;
	}
{% endfor %}

Emojis and More

Instead of displaying the digits from 1 to 9 it is possible to display any 9 characters. You can also use small images if you want to do that, either by using content: url(image1.png) or by showing/hiding img elements.

We will simply create an array of 9 elements with each element consisting of the character that will be displayed to represent a digit. That character could be a digit, letter, or emoji. Create themed puzzles with Halloween emojis, flags, food, animals, and more.

var halloween = ["😱","🎃","👻","🦇","⚰️","💀","🧙","🍭","🍫"];
var emojis = ["❤️","🙂","🐈","🐕","🍕","🔥","🏈","⛄","🚀"];
var food = ["🍇","🍉","🍑","🥨","🍕","🌮","🍪","🍿","🌭"];
var flags = ["🇲🇽","🇨🇦","🇳🇮","🇵🇦","🇨🇼","🇹🇼","🇯🇵","🇦🇺","🇮🇹"];
var animals = ["🐩","🦊","🦬","🐷","🦩","🐊","🦖","🦋","🐡"];

If you want to create puzzles that are not 3x3, the code can be extended to work for any sized rectangle. Larger sizes become unworkable with the number of buttons required. To make the puzzles lightweight enough for 4x4 puzzles you can significantly reduce the number of inputs and labels by only including the correct value. This repl creates much smaller puzzles.

With a bit more work it is possible to rewrite the code to generate non-rectangular blocks. There are not as many sources to generate other sizes so you can create your own if you want a different challenge.

Final Product

I have created several repls if you want to generate the simplest puzzles, emoji puzzles, or use Python with jinja2. You can play some puzzles at CSS Hole. If you like sudoku you can also try Sudoku Farm where you must complete sudoku puzzles while also not running out of resources. Of course, there is also a (resource-intensive) CSS-only version here.

Or play the 2x3 Halloween themed puzzle below.

You solved the puzzle!