In this blog, we will show you how to create a neomorphic Tic Tac Toe game using only HTML and CSS. Neomorphic design, also known as soft UI, is a trending design style that gives a 3D effect to the elements on the screen, making them appear more interactive and engaging. By the end of this tutorial, you’ll have a fully functional Tic Tac Toe game with a sleek neomorphic design that will take your game experience to the next level. So, let’s get started!
Before we start here are some more games you might like to create using css only –
- Neomorphic Tic Tac Toe Game using HTML and CSS Only
- Whac a mole game using HTML and CSS Only
- Plankman Game using HTML and CSS Only
- Rock Paper Scissor Game using HTML and CSS Only
- Carnival Target Practice Game using HTML and CSS Only
I would recommend you don’t just copy and paste the code, just look at the code and type by understanding it.
Demo
See the Pen Pure CSS/SVG Tic Tac Toe 😎 by Jhey (@jh3y) on CodePen.
HTML (PUG) Code
mixin confetti()
- let c = 0
while c < 10
.confetti(style=`--rotation: ${(Math.random() * 180) - 90}; --travel: ${Math.random() * -100};`) 🎉
- c++
mixin cross()
svg(style!=attributes.style class!=attributes.class viewBox="0 0 100 100")
path.cross(d="M 80 20 L 20 80" fill="none" stroke-width="10" stroke-linecap="round" stroke-dasharray="100" stroke-dashoffset="100")
path.cross(d="M 20 20 L 80 80" fill="none" stroke-width="10" stroke-linecap="round" stroke-dasharray="100" stroke-dashoffset="100")
mixin naught()
svg(class!=attributes.class style!=attributes.style viewBox="0 0 100 100")
circle(cx="50" cy="50" r="30" fill="none" stroke-width="10" stroke-linecap="round" stroke-dasharray="200" stroke-dashoffset="200")
form.game
- for (let i = 0; i < 9; i++)
input(type="checkbox" id=`x-${i}`)
- const x = i % 3
- const y = Math.floor(i / 3)
+cross()(class="x board__x" style=`--x: ${x}; --y: ${y};`)
path.cross(d="M 80 20 L 20 80" fill="none" stroke-width="10" stroke-linecap="round" stroke-dasharray="100" stroke-dashoffset="100")
path.cross(d="M 20 20 L 80 80" fill="none" stroke-width="10" stroke-linecap="round" stroke-dasharray="100" stroke-dashoffset="100")
input(type="checkbox" id=`o-${i}`)
+naught()(class="o board__o" style=`--x: ${x}; --y: ${y};`)
.board
- const DELAYS = [0.75, 0.5, 1, 0.25]
- for (let l = 0; l < 4; l++)
- const rotate = l % 2
- const shift = 2 % (l + 1) ? 1 : -1
- const delay = DELAYS[l]
svg.board__line(viewBox="0 0 10 300" style=`--rotate: ${rotate}; --shift: ${shift}; --delay: ${delay};`)
path(d="M 5 5 L 5 295" stroke-width="10" stroke-linecap="round" stroke-dasharray="300" stroke-dashoffset="300")
- for (let i = 0; i < 9; i++)
.board__cell
label(for=`x-${i}`)
+cross()(class="x ghost")
label(for=`o-${i}`)
+naught()(class="o ghost")
.game__result.game__result--x.result
+confetti()
.result__content
.result__title Winner!
+cross()(class="x result__winner")
.game__result.game__result--o.result
+confetti()
.result__content
.result__title Winner!
+naught()(class="o result__winner")
.game__result.game__result--draw.result
.result__content
.result__title Draw...
svg.zzz.result__winner(viewBox="0 0 24 24")
path(d="M23,12H17V10L20.39,6H17V4H23V6L19.62,10H23V12M15,16H9V14L12.39,10H9V8H15V10L11.62,14H15V16M7,20H1V18L4.39,14H1V12H7V14L3.62,18H7V20Z")
button(type="reset" title="Reset Board")
svg.reset(viewBox="0 0 24 24")
path(d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z")CSS (Stylus) Code
Create a file style.css and paste the code below.
@font-face {
font-family: Cyber;
src: url("https://assets.codepen.io/605876/Blender-Pro-Bold.otf");
font-display: swap;
}
*
box-sizing border-box
:root
--size 300px
--piece-size calc(var(--size) / 3)
--line hsl(0, 0%, 90%)
--bg hsl(210, 50%, 5%)
--naught hsl(150, 80%, 70%)
--naught-alpha hsla(150, 80%, 70%, 0.5)
--cross hsl(280, 80%, 70%)
--cross-alpha hsla(280, 80%, 70%, 0.5)
--draw-speed 0.15
--color hsl(0, 10%, 15%)
@media(min-width 768px)
--size 50vmin
@media(max-height 500px)
--size 300px
body
min-height 100vh
display grid
place-items center
margin 0
overflow hidden
background var(--bg)
color var(--theme-text-color)
font-family 'Cyber', sans-serif
text-transform uppercase
svg
filter drop-shadow(0 -0.25vmin 0.25vmin hsl(0, 10%, 0%)) drop-shadow(0 0 0.5vmin var(--alpha)) drop-shadow(0 0 1vmin var(--alpha)) drop-shadow(0 0 5vmin var(--stroke)) brightness(1.2)
stroke var(--stroke)
form
.board
display grid
place-items center
position relative
.game__result
display none
position absolute
width calc(var(--size) * 1.5)
height calc(var(--size) * 1.5)
transform translate(-50%, -50%)
top 50%
left 50%
label
.o
.x
position absolute
display inline-block
height var(--piece-size)
width var(--piece-size)
label
cursor pointer
&:hover .ghost
opacity 0.5
.ghost
opacity 0
transition opacity calc(var(--draw-speed) * 1s)
.o
--alpha var(--naught-alpha)
--stroke var(--naught)
transform rotateX(180deg)
.x
--alpha var(--cross-alpha)
--stroke var(--cross)
path:nth-of-type(2)
--delay var(--draw-speed)
:checked + .x
display block
:checked + .o
display block
.board
height var(--size)
width var(--size)
grid-template-columns repeat(3, 1fr)
grid-template-rows repeat(3, 1fr)
&__x
&__o
display none
left calc(var(--x) * (100% / 3))
top calc(var(--y) * (100% / 3))
z-index 2
position absolute
&__line
--stroke var(--line)
--alpha hsla(0, 0%, 90%, 0.5)
width calc(var(--size) * 0.05)
height var(--size)
position absolute
top 50%
left 50%
transform translate(-50%, -50%) rotate(calc(var(--rotate) * -90deg)) translate(calc(var(--shift) * ((var(--size) / 3) * 0.5)), 0)
&__cell
height var(--piece-size)
width var(--piece-size)
input
button
position absolute
button
top 125%
background transparent
border 0
padding 0
height 5vmin
width 5vmin
min-height 48px
min-width 48px
outline transparent
cursor pointer
display none
transition transform calc(var(--draw-speed) * 1s)
animation fadeIn calc(var(--draw-speed) * 4s) calc(var(--draw-speed) * 2s) both
&:hover
transform translate(0, -4%)
&:active
transform translate(0, 2%) scale(0.8)
.reset
height 100%
fill var(--line)
input
position fixed
left 100%
.result
animation flyIn calc(var(--draw-speed) * 3s) ease-in both
backdrop-filter blur(25px)
z-index 10
&__content
height 40%
width 40%
top 50%
left 50%
transform translate(-50%, -50%)
position absolute
border-radius 15%
background hsla(210, 30%, 20%, 0.8)
color hsl(0, 0%, 100%)
align-items center
display flex
justify-content center
flex-direction column
font-weight bold
font-size 2rem
box-shadow 0 3vmin 2.5vmin -2.5vmin hsl(0, 0%, 0%)
&__winner
position static
height calc(var(--size) / 3)
.zzz
--stroke hsl(210, 80%, 50%)
fill var(--stroke)
@keyframes fadeIn
from
opacity 0
@keyframes flyIn
from
opacity 0
transform translate(-50%, 250%) scale(0)
// This part only cares about showing/hiding the right labels for each move
:checked ~ .board .board__cell label:nth-of-type(odd)
:checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(odd)
:checked ~ :checked ~ :checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(odd)
:checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(odd)
:checked ~ :checked ~ .board .board__cell label:nth-of-type(even)
:checked ~ :checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(even)
:checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(even)
:checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(even)
// opacity 1
display block
:checked ~ .board .board__cell label:nth-of-type(even)
:checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(even)
:checked ~ :checked ~ :checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(even)
:checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(even)
:checked ~ :checked ~ .board .board__cell label:nth-of-type(odd)
:checked ~ :checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(odd)
:checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(odd)
:checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ .board .board__cell label:nth-of-type(odd)
// opacity 0.25
display none
// Winning combos
#x-0:checked ~ #x-1:checked ~ #x-2:checked ~ .game__result--x
#x-3:checked ~ #x-4:checked ~ #x-5:checked ~ .game__result--x
#x-6:checked ~ #x-7:checked ~ #x-8:checked ~ .game__result--x
#x-0:checked ~ #x-3:checked ~ #x-6:checked ~ .game__result--x
#x-1:checked ~ #x-4:checked ~ #x-7:checked ~ .game__result--x
#x-2:checked ~ #x-5:checked ~ #x-8:checked ~ .game__result--x
#x-0:checked ~ #x-4:checked ~ #x-8:checked ~ .game__result--x
#x-2:checked ~ #x-4:checked ~ #x-6:checked ~ .game__result--x
#o-0:checked ~ #o-1:checked ~ #o-2:checked ~ .game__result--o
#o-3:checked ~ #o-4:checked ~ #o-5:checked ~ .game__result--o
#o-6:checked ~ #o-7:checked ~ #o-8:checked ~ .game__result--o
#o-0:checked ~ #o-3:checked ~ #o-6:checked ~ .game__result--o
#o-1:checked ~ #o-4:checked ~ #o-7:checked ~ .game__result--o
#o-2:checked ~ #o-5:checked ~ #o-8:checked ~ .game__result--o
#o-0:checked ~ #o-4:checked ~ #o-8:checked ~ .game__result--o
#o-2:checked ~ #o-4:checked ~ #o-6:checked ~ .game__result--o
display flex
// Edge case if the last move is a winning move. Don't show the draw.
& ~ .game__result--draw
display none
& ~ button
display block
:checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked ~ :checked
// if the last move is played and there isn't a winner, show a draw
~ .game__result--draw
display block
~ button
display block
.board__line path
.o circle
.x path
animation draw calc(var(--draw-speed) * 1s) calc(var(--delay, 0) * 1s) ease-in both
@keyframes draw
to
stroke-dashoffset 0
.confetti
position absolute
top 50%
left 50%
font-size 2rem
animation celebrate 1s forwards, fadeOut calc(var(--draw-speed) * 1s) calc((1 - var(--draw-speed)) * 1s) forwards
@keyframes fadeOut
to
opacity 0
@keyframes celebrate
from
transform translate(-50%, -50%) rotate(calc(var(--rotation) * 1deg)) scale(0) translate(0, 0)
to
transform translate(-50%, -50%) rotate(calc(var(--rotation) * 1deg)) scale(1) translate(0, calc(var(--travel) * 1vmin))Output Till Now

Written by: Piyush Patil
If you found any mistakes or have any doubts please feel free to Contact Us
Hope you find this post helpful💖



