CSS-only Game: Tic-Tac-Toe

Một phiên bản Tic-Tac-Toe được xây dựng hoàn toàn bằng CSS hiện đại. Trò chơi khai thác trạng thái biểu mẫu và bộ chọn quan hệ để hiển thị X/O, phát hiện các tổ hợp chiến thắng, và gợi ý kết quả theo thời gian thực. Không dùng JavaScript cho gameplay — trọng tâm là sự tối giản, hiệu năng và tính diễn đạt của CSS.

CSS-only Game: Tic-Tac-Toe

Điểm nổi bật

  • Sử dụng :checked để lưu dấu lựa chọn trên từng ô.
  • Dùng :has() để nhận diện các line chiến thắng (hàng, cột, đường chéo).
  • Giao diện tối giản, tương thích mobile, nút Reset để bắt đầu ván mới.

Giới hạn

Phiên bản CSS-only không ép luật lượt đi hay khóa ô theo thứ tự X → O; mục tiêu là minh họa kỹ thuật CSS cho tương tác.

Demo: Tic-Tac-Toe (CSS-only)

CSS-only Tic-Tac-Toe

Chọn X hoặc O cho từng ô. Nhấn Reset để chơi ván mới.

X
O

X
O

X
O

X
O

X
O

X
O

X
O

X
O

X
O
CSS dùng:has()để phát hiện tổ hợp thắng.
X thắng
O thắng
Hòa

HTML

<div class="ttt" data-ttt>
  <div class="ttt-head">
    <h4 class="ttt-title">CSS-only Tic-Tac-Toe</h4>
    <p class="ttt-sub">Chọn X hoặc O cho từng ô. Nhấn Reset để chơi ván mới.</p>
  </div>

  <form class="ttt-form">
    <div class="ttt-board">
      <!-- 1 -->
      <div class="ttt-cell">
        <input type="radio" name="c1" id="c1x">
        <input type="radio" name="c1" id="c1o">
        <div class="ttt-choose">
          <label for="c1x" class="ttt-x">X</label>
          <label for="c1o" class="ttt-o">O</label>
        </div>
        <div class="ttt-mark ttt-mark-x">X</div>
        <div class="ttt-mark ttt-mark-o">O</div>
      </div>
      <!-- 2 -->
      <div class="ttt-cell">
        <input type="radio" name="c2" id="c2x">
        <input type="radio" name="c2" id="c2o">
        <div class="ttt-choose">
          <label for="c2x" class="ttt-x">X</label>
          <label for="c2o" class="ttt-o">O</label>
        </div>
        <div class="ttt-mark ttt-mark-x">X</div>
        <div class="ttt-mark ttt-mark-o">O</div>
      </div>
      <!-- 3 -->
      <div class="ttt-cell">
        <input type="radio" name="c3" id="c3x">
        <input type="radio" name="c3" id="c3o">
        <div class="ttt-choose">
          <label for="c3x" class="ttt-x">X</label>
          <label for="c3o" class="ttt-o">O</label>
        </div>
        <div class="ttt-mark ttt-mark-x">X</div>
        <div class="ttt-mark ttt-mark-o">O</div>
      </div>
      <!-- 4 -->
      <div class="ttt-cell">
        <input type="radio" name="c4" id="c4x">
        <input type="radio" name="c4" id="c4o">
        <div class="ttt-choose">
          <label for="c4x" class="ttt-x">X</label>
          <label for="c4o" class="ttt-o">O</label>
        </div>
        <div class="ttt-mark ttt-mark-x">X</div>
        <div class="ttt-mark ttt-mark-o">O</div>
      </div>
      <!-- 5 -->
      <div class="ttt-cell">
        <input type="radio" name="c5" id="c5x">
        <input type="radio" name="c5" id="c5o">
        <div class="ttt-choose">
          <label for="c5x" class="ttt-x">X</label>
          <label for="c5o" class="ttt-o">O</label>
        </div>
        <div class="ttt-mark ttt-mark-x">X</div>
        <div class="ttt-mark ttt-mark-o">O</div>
      </div>
      <!-- 6 -->
      <div class="ttt-cell">
        <input type="radio" name="c6" id="c6x">
        <input type="radio" name="c6" id="c6o">
        <div class="ttt-choose">
          <label for="c6x" class="ttt-x">X</label>
          <label for="c6o" class="ttt-o">O</label>
        </div>
        <div class="ttt-mark ttt-mark-x">X</div>
        <div class="ttt-mark ttt-mark-o">O</div>
      </div>
      <!-- 7 -->
      <div class="ttt-cell">
        <input type="radio" name="c7" id="c7x">
        <input type="radio" name="c7" id="c7o">
        <div class="ttt-choose">
          <label for="c7x" class="ttt-x">X</label>
          <label for="c7o" class="ttt-o">O</label>
        </div>
        <div class="ttt-mark ttt-mark-x">X</div>
        <div class="ttt-mark ttt-mark-o">O</div>
      </div>
      <!-- 8 -->
      <div class="ttt-cell">
        <input type="radio" name="c8" id="c8x">
        <input type="radio" name="c8" id="c8o">
        <div class="ttt-choose">
          <label for="c8x" class="ttt-x">X</label>
          <label for="c8o" class="ttt-o">O</label>
        </div>
        <div class="ttt-mark ttt-mark-x">X</div>
        <div class="ttt-mark ttt-mark-o">O</div>
      </div>
      <!-- 9 -->
      <div class="ttt-cell">
        <input type="radio" name="c9" id="c9x">
        <input type="radio" name="c9" id="c9o">
        <div class="ttt-choose">
          <label for="c9x" class="ttt-x">X</label>
          <label for="c9o" class="ttt-o">O</label>
        </div>
        <div class="ttt-mark ttt-mark-x">X</div>
        <div class="ttt-mark ttt-mark-o">O</div>
      </div>
    </div>

    <div class="ttt-status">
      <div class="ttt-hint">CSS dùng <code>:has()</code> để phát hiện tổ hợp thắng.</div>
      <div class="ttt-banner ttt-win-x">X thắng</div>
      <div class="ttt-banner ttt-win-o">O thắng</div>
      <div class="ttt-banner ttt-draw">Hòa</div>
      <div class="ttt-ctrl"><input type="reset" value="Reset"></div>
    </div>
  </form>
</div>

CSS

.ttt{--size:320px;--gap:6px;--line:#374151;--x:#ef4444;--o:#22c55e;--text:#e5e7eb;--accent:#3b82f6;display:grid;gap:14px;padding:18px;border:1px solid #1f2937;border-radius:16px;background:#0b1222;color:var(--text)}
.ttt-title{margin:0 0 4px;font:600 16px/1.2 system-ui;color:#fff}
.ttt-sub{margin:0;color:#9ca3af;font-size:12px}
.ttt-form{display:grid;gap:12px}
.ttt-board{position:relative;width:var(--size);height:var(--size);max-width:100%;aspect-ratio:1;margin:auto;display:grid;grid-template-columns:repeat(3,1fr);gap:var(--gap);background:
  linear-gradient(var(--line),var(--line)) center/100% 1px no-repeat,
  linear-gradient(var(--line),var(--line)) center/100% 1px no-repeat,
  linear-gradient(90deg,var(--line),var(--line)) center/1px 100% no-repeat,
  linear-gradient(90deg,var(--line),var(--line)) center/1px 100% no-repeat;
background-position:
  50% calc(33.333% - var(--gap)/2),
  50% calc(66.666% + var(--gap)/2),
  calc(33.333% - var(--gap)/2) 50%,
  calc(66.666% + var(--gap)/2) 50%}
.ttt-cell{position:relative;background:#0b1222;border-radius:12px;box-shadow:inset 0 0 0 1px #182032,inset 0 6px 18px rgba(0,0,0,.35);display:grid;place-items:center;overflow:hidden}
.ttt-cell input[type=radio]{position:absolute;inset:0;opacity:0}
.ttt-choose{position:absolute;inset:0;display:grid;grid-template-rows:1fr 1fr;z-index:2}
.ttt-choose label{display:grid;place-items:center;cursor:pointer;user-select:none;font-weight:800;font-size:clamp(20px,7vw,36px);color:#9ca3af;transition:background .2s,color .2s}
.ttt-choose .ttt-x{border-bottom:1px dashed #1f2a44}
.ttt-choose .ttt-o{border-top:1px dashed #1f2a44}
.ttt-choose label:hover{background:rgba(59,130,246,.08);color:#cbd5e1}
.ttt-mark{position:absolute;inset:0;display:grid;place-items:center;font-weight:900;font-size:clamp(34px,10vw,64px);opacity:0;transform:scale(.9);transition:opacity .25s,transform .25s}
.ttt-mark-x{color:var(--x);text-shadow:0 0 24px rgba(239,68,68,.2)}
.ttt-mark-o{color:var(--o);text-shadow:0 0 24px rgba(34,197,94,.2)}
#c1x:checked~.ttt-mark-x,#c2x:checked~.ttt-mark-x,#c3x:checked~.ttt-mark-x,#c4x:checked~.ttt-mark-x,#c5x:checked~.ttt-mark-x,#c6x:checked~.ttt-mark-x,#c7x:checked~.ttt-mark-x,#c8x:checked~.ttt-mark-x,#c9x:checked~.ttt-mark-x{opacity:1;transform:scale(1)}
#c1o:checked~.ttt-mark-o,#c2o:checked~.ttt-mark-o,#c3o:checked~.ttt-mark-o,#c4o:checked~.ttt-mark-o,#c5o:checked~.ttt-mark-o,#c6o:checked~.ttt-mark-o,#c7o:checked~.ttt-mark-o,#c8o:checked~.ttt-mark-o,#c9o:checked~.ttt-mark-o{opacity:1;transform:scale(1)}
#c1x:checked~.ttt-choose,#c1o:checked~.ttt-choose,#c2x:checked~.ttt-choose,#c2o:checked~.ttt-choose,#c3x:checked~.ttt-choose,#c3o:checked~.ttt-choose,#c4x:checked~.ttt-choose,#c4o:checked~.ttt-choose,#c5x:checked~.ttt-choose,#c5o:checked~.ttt-choose,#c6x:checked~.ttt-choose,#c6o:checked~.ttt-choose,#c7x:checked~.ttt-choose,#c7o:checked~.ttt-choose,#c8x:checked~.ttt-choose,#c8o:checked~.ttt-choose,#c9x:checked~.ttt-choose,#c9o:checked~.ttt-choose{pointer-events:none;opacity:.25}
.ttt-status{display:grid;gap:10px;background:#0e1426;padding:10px;border-radius:10px;border:1px solid #1e2a3a}
.ttt-hint{color:#a1a1aa;font-size:12px}
.ttt-banner{display:none;padding:8px 10px;border-radius:8px;font-weight:700;border:1px solid #223052}
.ttt-win-x{color:#ef4444}
.ttt-win-o{color:#22c55e}
.ttt-draw{color:#f59e0b}
.ttt-ctrl input[type=reset]{appearance:none;border:1px solid #25324a;background:#0e172b;color:#e5e7eb;padding:8px 12px;border-radius:10px;font-weight:600;cursor:pointer}
.ttt-ctrl input[type=reset]:hover{border-color:var(--accent)}
/* win X */
.ttt-board:has(#c1x:checked):has(#c2x:checked):has(#c3x:checked) ~ .ttt-status .ttt-win-x,
.ttt-board:has(#c4x:checked):has(#c5x:checked):has(#c6x:checked) ~ .ttt-status .ttt-win-x,
.ttt-board:has(#c7x:checked):has(#c8x:checked):has(#c9x:checked) ~ .ttt-status .ttt-win-x,
.ttt-board:has(#c1x:checked):has(#c4x:checked):has(#c7x:checked) ~ .ttt-status .ttt-win-x,
.ttt-board:has(#c2x:checked):has(#c5x:checked):has(#c8x:checked) ~ .ttt-status .ttt-win-x,
.ttt-board:has(#c3x:checked):has(#c6x:checked):has(#c9x:checked) ~ .ttt-status .ttt-win-x,
.ttt-board:has(#c1x:checked):has(#c5x:checked):has(#c9x:checked) ~ .ttt-status .ttt-win-x,
.ttt-board:has(#c3x:checked):has(#c5x:checked):has(#c7x:checked) ~ .ttt-status .ttt-win-x{display:block}
/* win O */
.ttt-board:has(#c1o:checked):has(#c2o:checked):has(#c3o:checked) ~ .ttt-status .ttt-win-o,
.ttt-board:has(#c4o:checked):has(#c5o:checked):has(#c6o:checked) ~ .ttt-status .ttt-win-o,
.ttt-board:has(#c7o:checked):has(#c8o:checked):has(#c9o:checked) ~ .ttt-status .ttt-win-o,
.ttt-board:has(#c1o:checked):has(#c4o:checked):has(#c7o:checked) ~ .ttt-status .ttt-win-o,
.ttt-board:has(#c2o:checked):has(#c5o:checked):has(#c8o:checked) ~ .ttt-status .ttt-win-o,
.ttt-board:has(#c3o:checked):has(#c6o:checked):has(#c9o:checked) ~ .ttt-status .ttt-win-o,
.ttt-board:has(#c1o:checked):has(#c5o:checked):has(#c9o:checked) ~ .ttt-status .ttt-win-o,
.ttt-board:has(#c3o:checked):has(#c5o:checked):has(#c7o:checked) ~ .ttt-status .ttt-win-o{display:block}
/* draw */
.ttt-board:has(#c1x:checked, #c1o:checked)
  :has(#c2x:checked, #c2o:checked)
  :has(#c3x:checked, #c3o:checked)
  :has(#c4x:checked, #c4o:checked)
  :has(#c5x:checked, #c5o:checked)
  :has(#c6x:checked, #c6o:checked)
  :has(#c7x:checked, #c7o:checked)
  :has(#c8x:checked, #c8o:checked)
  :has(#c9x:checked, #c9o:checked)
  :not(:has(#c1x:checked):has(#c2x:checked):has(#c3x:checked),
       :has(#c4x:checked):has(#c5x:checked):has(#c6x:checked),
       :has(#c7x:checked):has(#c8x:checked):has(#c9x:checked),
       :has(#c1x:checked):has(#c4x:checked):has(#c7x:checked),
       :has(#c2x:checked):has(#c5x:checked):has(#c8x:checked),
       :has(#c3x:checked):has(#c6x:checked):has(#c9x:checked),
       :has(#c1x:checked):has(#c5x:checked):has(#c9x:checked),
       :has(#c3x:checked):has(#c5x:checked):has(#c7x:checked),
       :has(#c1o:checked):has(#c2o:checked):has(#c3o:checked),
       :has(#c4o:checked):has(#c5o:checked):has(#c6o:checked),
       :has(#c7o:checked):has(#c8o:checked):has(#c9o:checked),
       :has(#c1o:checked):has(#c4o:checked):has(#c7o:checked),
       :has(#c2o:checked):has(#c5o:checked):has(#c8o:checked),
       :has(#c3o:checked):has(#c6o:checked):has(#c9o:checked),
       :has(#c1o:checked):has(#c5o:checked):has(#c9o:checked),
       :has(#c3o:checked):has(#c5o:checked):has(#c7o:checked))
  ~ .ttt-status .ttt-draw{display:block}

JS

// Không cần JavaScript cho phiên bản CSS-only này.

Kết luận

CSS đủ mạnh để dựng nên những trải nghiệm tương tác vừa trực quan vừa mượt mà. Tic-Tac-Toe là ví dụ ngắn gọn cho cách CSS quản lý trạng thái và chuyển động, mở lối cho các ý tưởng game/interactive nhẹ, đẹp và giàu tính học hỏi.

Bình luận


  • Không có bình luận.

Init Toolbox

Nhấn Ctrl + \ trên máy tính, hoặc vuốt sang trái ở bất kỳ đâu trên mobile.

Đăng nhập





Đang tải...