Đ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.
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