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")
View Compiled
@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(--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))
View Compiled
// 404
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.