So, you’ve set up your LWC project and you’re itching to get something on the screen. Not just anything - you're building a game. A classic. A battle of Xs and Os. That’s right, it's time to bring Tic-Tac-Toe to life using Lightning Web Components (LWC).
In this article, we’ll focus on constructing the visual foundation of the game - the board itself. No click events, no win logic just yet. We'll get to that in the next article. Today is all about getting the board on screen, laying out your components properly, and understanding how these little building blocks work together.
Component Architecture: A Breakdown
Let’s take a moment to zoom out and see what we’re working with. Think of your app like a well-organized Lego set. Each piece (component) has its role, and when they’re combined, they build something interactive and fun.
Here's the component structure so far:
ticTacToe
: The outer shell. Just a wrapper for now.
gameContainer
: Holds the game logic and state (for now, just the board cells).
boardComponent
: Translates that 1D array of cells into a 3x3 grid.
cellComponent
: Displays each individual cell.
We’re building a board-but doing it the LWC way: modular, declarative, and reactive.
ticTacToe: The Game Shell
Let’s start at the top.
<template>
<div class="board">
<c-game-container></c-game-container>
</div>
</template>
import { LightningElement } from 'lwc';
export default class TicTacToe extends LightningElement {}
The ticTacToe
component is minimal for now, but its purpose is crucial - it acts as the entry point for the entire app. Think of it as your game window or canvas. We’ve added some basic CSS to center it on the page:
.board {
max-width: 300px;
margin: auto;
}
Small touch, big impact. Centering your board makes everything feel more polished and intentional.
gameContainer: The Brain Behind the Board
Next up, the gameContainer
. This is where we initialize the game’s state - in this case, the board itself.
<template>
<!-- Lightning card component used to display the Tic-Tac-Toe game -->
<lightning-card title="Tic-Tac-Toe">
<div class="slds-p-horizontal_x-small">
<!-- Custom board component that renders the game board, cells are passed as a property -->
<c-board-component cells={cells}></c-board-component>
</div>
</lightning-card>
</template>
import { LightningElement, track } from 'lwc';
import { generateInitialBoard } from 'c/gameLogic';
export default class GameContainer extends LightningElement {
@track cells = generateInitialBoard();
}
Here’s where the real magic starts. generateInitialBoard()
returns a 9-cell array, each with an id
and an empty value
. This is your game state. Nothing fancy yet - just 9 blank cells waiting to become Xs or Os.
/**
* @description Function to generate the initial state of the Tic-Tac-Toe board
* @return {Array} Creates an array of 9 objects, each representing a cell with a unique id and an empty value
*/
export function generateInitialBoard() {
// Create an array of 9 objects, each representing a cell on the board
// Each cell has a unique id and an empty value
return Array.from({length: 9}, (_, index) => ({
id: index, // Unique identifier for the cell
value: '' // Initial value of the cell (empty)
}));
}
Neat, right? This is future-proof too - you’ll use value
soon when we add click handling.
boardComponent: Turning a Flat Array into a Grid
Right now, we have 9 objects in an array. But how do you transform that into a 3x3 layout on the UI? Enter: boardComponent
.
<template>
<!-- Iterate over the rows array, where each row represents a group of cells -->
<template for:each={rows} for:item="row">
<!-- Use lightning-layout to structure the row -->
<lightning-layout key={row.id}>
<!-- Iterate over the cells in the current row -->
<template for:each={row.cells} for:item="cell">
<!-- Use lightning-layout-item to structure each cell and assign a unique key -->
<lightning-layout-item key={cell.id} size="4" flexibility="auto">
<!-- Render the cell component and pass the cell id as a property -->
<c-cell-component cell-id={cell.id}></c-cell-component>
</lightning-layout-item>
</template>
</lightning-layout>
</template>
</template>
This is where Lightning’s layout system shines. It’s like building a responsive grid, but inside Salesforce. Each row gets split into three cells using lightning-layout-item
with size="4"
(because 3x4 = 12 columns - a standard SLDS grid row).
And how do we split the flat array into rows?
import { LightningElement, api } from 'lwc';
export default class BoardComponent extends LightningElement {
// Array of cells passed as a property to the component
@api cells = [];
/**
* @description Getter to transform the flat array of cells into rows of 3 cells each
* @return {Array} An array of row objects, where each row contains an id and an array of 3 cells
*/
get rows() {
const rows = [];
// Iterate through the cells array in steps of 3 to create rows
for (let i = 0; i < this.cells.length; i += 3) {
rows.push({
id: `row-${i / 3}`, // Unique identifier for each row
cells: this.cells.slice(i, i + 3) // Extract 3 cells for the current row
});
}
return rows; // Return the array of rows
}
}
Nice and clean. This getter slices the array into three rows of three cells each. Perfect for rendering.
cellComponent: The Smallest Brick
Last but not least, let’s zoom into the individual cells.
<template>
<div class="slds-m-around_xxx-small cell">
<span>{cellId}</span>
</div>
</template>
import { LightningElement, api } from 'lwc';
export default class CellComponent extends LightningElement {
@api cellId;
}
Right now, it’s just showing the cellId
, which is useful for debugging. But soon this will become clickable, display values (X or O), and trigger game logic.
Some simple styling goes a long way:
.cell {
height: 80px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ccc;
font-size: 1.5rem;
}
This gives you a neat 80x80 box with centered text. Combine nine of these and you’ve got yourself a game board.
Putting It All Together
So what do we have by the end of this?
- A centered, responsive 3x3 board.
- A clear, modular component structure.
- State initialized and passed down via props.
- A base that’s ready for interaction, rules, and flair.
If you launch this in your Salesforce org, you’ll see a beautiful blank board with IDs 0–8. It’s simple, but don’t underestimate it - this is the foundation. Everything else you add later (clicks, turns, win detection) builds on this.
Why It Matters
You might be wondering - why not just use one component and build the board in there? Great question.
Using separate components keeps your logic clean, your structure scalable, and your code reusable. Down the road, if you want to animate a cell, highlight a winning combo, or add extra metadata, you’ll thank yourself for isolating the logic this way.
Conclusion
You did it! You’ve just built the board for your Tic-Tac-Toe game using Lightning Web Components. Sure, it doesn’t do much yet - but it’s real, it’s modular, and it’s exactly what you need to build something fun and interactive on the Salesforce platform.
In the next article, we’ll dive into click handling and state management, where each cell becomes alive with user input.
Until then, keep the code clean, the components light, and the logic sharp.
Ready for the next move?