Neomorphic Tic Tac Toe Game using HTML and CSS Only

Share your love

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 –

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

neomorphic tic tac toe game using html and css

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💖

Share your love