This project demonstrates basic logic gates and latches using only HTML and CSS.
<input type="checkbox"> <label for="id"><label>
:checked
"- way to check input value
:not()
" - acts as not gate
,
" - acts as or gate (write conditions separated by comma)
display:none | block
" - can be used to toggle presence and absence of any element from the DOM
<div class="gate" title="AND gate">
<div class="gate-inputs">
<input type="checkbox" id="and-in1" /> <label for="and-in1">1</label>
<input type="checkbox" id="and-in2" /> <label for="and-in2">2</label>
</div>
<div class="gate-name">AND</div>
<div class="gate-outputs">
<div class="output-light and-output"></div>
</div>
</div>
.gate:has(#and-in1:checked):has(#and-in2:checked) .and-output {
background: #4caf50;
box-shadow: var(--glow);
}
The AND gate turns green only when #and-in1
and #and-in2
are both checked.
<div class="gate" title="OR gate">
<div class="gate-inputs">
<input type="checkbox" id="or-in1" /> <label for="or-in1">1</label>
<input type="checkbox" id="or-in2" /> <label for="or-in2">2</label>
</div>
<div class="gate-name">OR</div>
<div class="gate-outputs">
<div class="output-light or-output"></div>
</div>
</div>
.gate:has(#or-in1:checked) .or-output,
.gate:has(#or-in2:checked) .or-output {
background: #4caf50;
box-shadow: var(--glow);
}
The OR gate turns green when #or-in1
is checked and when #or-in2
is checked.
<div class="gate" title="NOT gate">
<div class="gate-inputs">
<input type="checkbox" id="not-in" /> <label for="not-in">Input</label>
</div>
<div class="gate-name">NOT</div>
<div class="gate-outputs">
<div class="output-light not-output"></div>
</div>
</div>
.gate:not(:has(#not-in:checked)) .not-output {
background: #4caf50;
box-shadow: var(--glow);
}
The NOT gate turns green only when #not-in
is not checked.
<div class="gate" title="XOR gate">
<div class="gate-inputs">
<input type="checkbox" id="xor-in1" /> <label for="xor-in1">1</label>
<input type="checkbox" id="xor-in2" /> <label for="xor-in2">2</label>
</div>
<div class="gate-name">XOR</div>
<div class="gate-outputs">
<div class="output-light xor-output"></div>
</div>
</div>
.gate:has(#xor-in1:checked):not(:has(#xor-in2:checked)) .xor-output,
.gate:has(#xor-in2:checked):not(:has(#xor-in1:checked)) .xor-output {
background: #4caf50;
box-shadow: var(--glow);
}
The XOR gate turns green when only one of #xor-in1
and #xor-in2
is checked.
<div class="gate" title="NXOR (XNOR) gate">
<div class="gate-inputs">
<input type="checkbox" id="nxor-in1" /> <label for="nxor-in1">1</label>
<input type="checkbox" id="nxor-in2" /> <label for="nxor-in2">2</label>
</div>
<div class="gate-name">NXOR (XNOR)</div>
<div class="gate-outputs">
<div class="output-light nxor-output"></div>
</div>
</div>
.gate:has(#nxor-in1:checked):has(#nxor-in2:checked) .nxor-output,
.gate:not(:has(#nxor-in1:checked)):not(:has(#nxor-in2:checked)) .nxor-output {
background: #4caf50;
box-shadow: var(--glow);
}
The NXOR gate turns green when both of #nxor-in1
and #nxor-in2
have same value.
<div class="gate" title="NOR gate">
<div class="gate-inputs">
<input type="checkbox" id="nor-in1" /> <label for="nor-in1">1</label>
<input type="checkbox" id="nor-in2" /> <label for="nor-in2">2</label>
</div>
<div class="gate-name">NOR</div>
<div class="gate-outputs">
<div class="output-light nor-output"></div>
</div>
</div>
.gate:not(:has(#nor-in1:checked)):not(:has(#nor-in2:checked)) .nor-output {
background: #4caf50;
box-shadow: var(--glow);
}
NOR(a,b) = AND(NOT(a),NOT(b))
<div class="gate" title="NAND gate">
<div class="gate-inputs">
<input type="checkbox" id="nand-in1" /> <label for="nand-in1">1</label>
<input type="checkbox" id="nand-in2" /> <label for="nand-in2">2</label>
</div>
<div class="gate-name">NAND</div>
<div class="gate-outputs">
<div class="output-light nand-output"></div>
</div>
</div>
.gate:not(:has(#nand-in1:checked):has(#nand-in2:checked)) .nand-output {
background: #4caf50;
box-shadow: var(--glow);
}
NAND(a,b) = NOT(AND(a,b))
<div class="gate" title="T Latch">
<div class="gate-inputs">
<input type="checkbox" id="t-a" />
<label for="t-a" class="t-toggle">T</label>
<input type="checkbox" id="t-b" />
<label for="t-b" class="t-toggle">T</label>
</div>
<div class="gate-name">T Latch</div>
<div class="gate-outputs">
<div class="output-light t-output"></div>
</div>
</div>
/*
T - Latch
["0", gray] [0, _] -> 0
[1, _] ["0", green] -> 1
["1", gray] [1, _] -> 1
[0, _] ["1", green] -> 0
*/
.gate:not(:has(#t-a:checked)):not(:has(#t-b:checked)) label[for="t-b"],
.gate:has(#t-a:checked):not(:has(#t-b:checked)) label[for="t-a"],
.gate:has(#t-a:checked):has(#t-b:checked) label[for="t-b"],
.gate:not(:has(#t-a:checked)):has(#t-b:checked) label[for="t-a"] {
display: none;
}
.gate:has(#t-a:checked) .t-output {
background: #4caf50;
box-shadow: var(--glow);
}
input[type="checkbox"]:checked + label[for="t-a"] {
background: var(--labelBG);
color: gray;
box-shadow: inset 0 0 0 3px #aaa;
}
input[type="checkbox"]:checked + label[for="t-a"]:hover {
background: #ccc;
box-shadow: inset 0 0 0 3px #888;
}
label[for="t-b"] {
background: #4caf50 !important;
color: white;
box-shadow: inset 0 0 0 3px #388e3c;
}
A T latch toggles its state when you turn it on, but holds the state when it's off.
But CSS can't store valuesβ¦ or can it? π€
Here's the trick: since you're only allowed to toggle one button at a time, this setup creates a loop of four states:
[0 βΆ 0] β [1 βΆ 1] β β [1 βΆ 0] β [0 βΆ 1]
The switch happens every second click. But a checkbox normally toggles on every click, so how do we fix that?
βΈ The solution is: every click swaps which button is visible.
βΈ Only one of those buttons (the first one) actually changes the output.
βΈ The other one is just a decoy to keep the toggle pattern going.
To complete the trick visually:
the button that controls the output is styled as if it's off,
and the other one is styled as if it's on.
Bonus insight: if you observe, the decoy (#t-b
) acts as the the previous value of the output
and that is exactly how T latches work!
<div class="gate" title="SR Latch">
<form class="gate-inputs">
<input type="checkbox" id="sr-t" checked/> <label for="sr-t" title="Set (S)">S</label>
<input type="checkbox" id="sr-s"/> <label for="sr-s" title="Set (S)">S</label>
<input type="checkbox" id="sr-r"/> <label for="sr-r" title="Reset (R)">R</label>
<button type="reset" class="reset-btn" title="Reset (R)">R</button>
</form>
<div class="gate-name">SR Latch</div>
<div class="gate-outputs">
<div class="output-light sr-output1"></div>
<div class="output-light sr-output2"></div>
</div>
</div>
/*
SR - Latch
[0, _] ["0", red] ["1", green] -> 0 (4th sate)
t - s - r β¨
β [1, _] ["0", gray] ["0", gray] -> 0 (0th sate)β°
(1st sate) (3rd sate)
["1", green] [1, _] ["0", red] -> 1 ["0", red] [1, _] ["1", green] -> 0
β³β¬ (2nd sate) ["0", gray] [1, _] ["0", gray] -> 1 β¬
*/
label[for="sr-t"],
.reset-btn,
#sr-r:checked + label[for="sr-r"],
#sr-s:checked + label[for="sr-s"] {
display: none;
}
.gate:has(#sr-s:checked) label[for="sr-t"] {
display: block;
}
.gate:has(#sr-t:checked):has(#sr-s:checked) label[for="sr-r"],
.gate:has(#sr-r:checked) input[type="checkbox"]:not(:checked) + label[for="sr-s"],
.gate:has(#sr-r:checked) input[type="checkbox"]:not(:checked) + label[for="sr-t"] {
background: #f44336;
color: white;
box-shadow: inset 0 0 0 3px #c62828;
pointer-events: none;
opacity: 0.7;
cursor: not-allowed;
}
.gate:has(#sr-r:checked) .reset-btn {
all: unset;
padding: 12px 0;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
width: 48px;
text-align: center;
transition: all 0.25s;
background: #4caf50;
color: white;
box-shadow: inset 0 0 0 3px #388e3c;
}
.gate:has(#sr-s:checked):not(:has(#sr-r:checked)) .sr-output1,
.gate:not(:has(#sr-s:checked):not(:has(#sr-r:checked))) .sr-output2 {
background: #4caf50;
box-shadow: var(--glow);
}
The SR latch has two inputs: S (Set) and R (Reset).
S stores the value, and can't take it back - only R can.
Using a similar strategy to the T latch (with a decoy input#sr-t
), we apply the trick again β but this time the decoy is active by default.
Here's how the state transitions map out:
"" β shown and clickable, '' β shown but not clickable T S R [1 '0' "1" βΆ 0] β [1 "0" "0" βΆ 0] β ['0' 1 "1" βΆ 0] β β ["1" 1 '0' βΆ 1] β ["0" 1 "0" βΆ 1]
#sr-s
is on but #sr-r
is not.#sr-r
goes from on to off it goes back to the start.#sr-r
and show the reset button and for this we need the inputs to be inside a form.pointer-events: none;
<select id="mode-selector">
<option selected>default</option>
<option value="debug">inspect</option>
</select>
body:has(#mode-selector option[value="debug"]:checked) input[type="checkbox"] {
display: inline-block;
}
body:has(#mode-selector option[value="debug"]:checked) .reset-btn::after {
content: "eset";
}
A mode selector lets you choose between views (e.g. default vs inspect/debug):
<select id="theme-selector">
<option value="system" selected>System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
:root:has(#theme-selector option[value="dark"]:checked) {
--bodyBG: #121212;
--mainBG: #1e1e1e;
--labelBG: #333;
--modalText: #eee;
}
@media (prefers-color-scheme: dark) {
:root:has(#theme-selector option[value="system"]:checked) {
--bodyBG: #121212;
--mainBG: #1e1e1e;
--labelBG: #333;
--modalText: #eee;
}
}
Light/dark mode toggle with CSS:
Implemented using a hidden checkbox and radios: The rest is for you to discover!
To know more you can just open your dev tools with Ctrl+Shift+I or F12 or add view-source: before the url to view the source code.