Back to Tutorials
Java Swing

Tic Tac Toe Game using Java Swing

Build a fully playable, interactive Tic Tac Toe GUI game in Java from scratch - complete with click handling, win detection, draw detection, and a restart button. No libraries, just pure Java Swing.

~45–60 mins Intermediate Java Swing GUI
What You'll Build
Tech / Tools Used: Java JDK 8 or above, Java Swing (built-in, no extra libraries), any Java IDE or terminal
What You Need
  • Java JDK 8 or later
  • Any Java IDE or text editor
  • A terminal / command prompt
  • No external libraries needed
Recommended IDE
  • IntelliJ IDEA (Community – free)
  • Eclipse IDE (free)
  • VS Code + Java Extension Pack
  • Notepad++ + terminal (simple)
Step 1

Setup - Install Java and Prepare Your Project

1.1 Check if Java is Already Installed

Open your terminal (Command Prompt on Windows, Terminal on Mac/Linux) and type:

Terminal
java -version

If you see an output like java version "17.0.x" or any version above 8, you are ready. If you see an error saying Java is not recognized, follow the next step.

1.2 Install Java JDK (if not installed)

  1. Go to adoptium.net (formerly AdoptOpenJDK) or oracle.com/java/technologies/downloads.
  2. Download the Java JDK 17 LTS (Long-Term Support) installer for your operating system.
  3. Run the installer and follow the setup wizard. Accept all default settings.
  4. After installation, close and reopen your terminal and run java -version again. You should now see a version number.
On Windows, if java -version still doesn't work after installation, you may need to set the JAVA_HOME environment variable. Search "How to set JAVA_HOME on Windows" - it's a quick 2-minute fix.

1.3 Create Your Project File

  1. Create a new folder on your computer. Name it TicTacToeGame.
  2. Inside that folder, create a new file called TicTacToe.java. This will hold all our code - the entire game fits in one file.

1.4 How to Compile and Run

We will use the terminal to compile and run the game. Here's what the two commands do:

Terminal
# Step 1: Navigate into your project folder
cd TicTacToeGame

# Step 2: Compile the Java file (creates TicTacToe.class)
javac TicTacToe.java

# Step 3: Run the compiled program (opens the game window)
java TicTacToe
Tip for IDE users: If you are using IntelliJ IDEA or Eclipse, just create a new Java project, paste the code into the main class file, and press the green Run button. No terminal needed!
Step 2

Understand the Structure - Java Swing & Game Logic Explained

This is the most important step. Before you write even one line of code, you should understand what tools we are using and how the game logic works. Read this section carefully - it will make the code feel obvious rather than confusing.


2.1 What is Java Swing?

Java Swing is a built-in Java library for building Graphical User Interfaces (GUIs) - that means windows, buttons, text labels, input fields, and all the visual things you click on in a desktop application. You do not need to install anything; it ships with the Java JDK.

Think of Swing as a toolkit of ready-made building blocks. You pick the blocks you need (a window, buttons, a grid), place them how you want, and Java handles the drawing and interaction for you.


2.2 The Key Swing Components We'll Use

We only need 4 core Swing components to build this entire game:

JFrame

The main application window. Everything lives inside JFrame. Think of it as the empty picture frame - you put everything else inside it.

JButton

A clickable button. Each cell of our 3×3 Tic Tac Toe grid is one JButton. When a player clicks a button, it shows X or O.

JLabel

A text display. We use one JLabel at the top to show messages like "Player X's Turn" or "Player O Wins!"

GridLayout

A layout manager that automatically arranges components in rows and columns - perfect for our 3×3 game board.


2.3 What is an ActionListener?

When a player clicks a button, Java needs to know what to do. That's where ActionListener comes in.

An ActionListener is like a guard standing next to every button. When the button is clicked, the guard immediately says "hey, something happened!" and runs a specific piece of code (the actionPerformed method). In our game, that code will: check if the cell is empty, mark it X or O, check for a win, and switch turns.

Analogy: Imagine a vending machine. You press a button (the event). The machine detects that press (ActionListener), then performs the action - drops your snack. Our buttons work exactly the same way.

2.4 - The Game Board Structure

We represent the 3×3 grid as a 1D array of 9 JButtons. The positions map to the grid like this:

[0] | [1] | [2] -----+-----+----- [3] | [4] | [5] -----+-----+----- [6] | [7] | [8]

Using a 1D array keeps the code simple. We just loop through indices 0 to 8 to set up or reset all 9 cells.


2.5 Win Condition Logic

In Tic Tac Toe there are 8 possible ways to win: 3 rows, 3 columns, and 2 diagonals. We store these as a list of index groups and check them after every move:

Winning combinations (index positions): Rows : [0,1,2] | [3,4,5] | [6,7,8] Columns : [0,3,6] | [1,4,7] | [2,5,8] Diagonals : [0,4,8] | [2,4,6]

After every click, we loop through all 8 combinations and check if all 3 positions in any combination have the same player's mark (X or O). If yes - that player wins!


2.6 Component and Method Overview

Component / MethodWhat It Does
JFrame frame Creates the main game window. We set its size, title, and close behavior here.
JButton[] buttons Array of 9 buttons representing the 3×3 game grid. Each button is one cell.
JLabel statusLabel Shows the current game state - whose turn it is, the winner, or a draw message.
JButton restartBtn A separate button outside the grid. Resets all 9 cells and restarts the game.
boolean xTurn A flag that tracks whose turn it currently is. true = Player X, false = Player O.
boolean gameOver Becomes true when the game ends (win or draw). Prevents further clicks.
actionPerformed() Runs every time a grid button is clicked. Marks the cell, checks for a win or draw, and switches turns.
checkWinner() Loops through all 8 winning combinations and checks if any player has won.
resetGame() Clears all button text, resets the turn to X, and sets gameOver back to false.
Step 3

Write the Full Code

Copy the complete code below into your TicTacToe.java file. Every section is clearly commented so you know what each block does as you read.

TicTacToe.java
// ============================================================
// Tic Tac Toe Game using Java Swing
// NKDevSpace Tutorial
// ============================================================

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class TicTacToe implements ActionListener {

    // -- WINDOW & PANELS ──────────────────────────────────────
    JFrame frame;
    JPanel titlePanel;   // Top area: status label
    JPanel boardPanel;   // Middle area: 3x3 button grid
    JPanel bottomPanel;  // Bottom area: restart button

    // -- COMPONENTS ───────────────────────────────────────────
    JLabel statusLabel;      // Shows game status text
    JButton[] buttons;       // The 9 grid cells
    JButton restartButton;   // Reset/restart button

    // -- GAME STATE ────────────────────────────────────────────
    boolean xTurn = true;    // true = X's turn, false = O's turn
    boolean gameOver = false; // Stops clicks after game ends

    // -- WIN COMBINATIONS ─────────────────────────────────────
    // All 8 ways to win: rows, columns, diagonals
    int[][] winCombos = {
        {0, 1, 2},  // Top row
        {3, 4, 5},  // Middle row
        {6, 7, 8},  // Bottom row
        {0, 3, 6},  // Left column
        {1, 4, 7},  // Middle column
        {2, 5, 8},  // Right column
        {0, 4, 8},  // Diagonal top-left to bottom-right
        {2, 4, 6}   // Diagonal top-right to bottom-left
    };

    // ============================================================
    // CONSTRUCTOR - Called when the program starts
    // Sets up the entire window and game board
    // ============================================================
    public TicTacToe() {

        // ----- 1. Create the main window -----
        frame = new JFrame("Tic Tac Toe | NKDevSpace");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(500, 550);
        frame.setLocationRelativeTo(null); // Center on screen
        frame.setLayout(new BorderLayout());
        frame.setResizable(false);

        // ----- 2. Status label at the top -----
        statusLabel = new JLabel("Player X's Turn");
        statusLabel.setFont(new Font("Poppins", Font.BOLD, 20));
        statusLabel.setForeground(Color.WHITE);
        statusLabel.setHorizontalAlignment(JLabel.CENTER);
        statusLabel.setOpaque(true);
        statusLabel.setBackground(new Color(30, 37, 53));
        statusLabel.setPreferredSize(new Dimension(500, 60));

        titlePanel = new JPanel();
        titlePanel.setBackground(new Color(30, 37, 53));
        titlePanel.add(statusLabel);

        // ----- 3. Create the 3x3 button grid -----
        boardPanel = new JPanel();
        boardPanel.setLayout(new GridLayout(3, 3, 6, 6)); // 3 rows, 3 cols, 6px gaps
        boardPanel.setBackground(new Color(42, 51, 72));
        boardPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

        buttons = new JButton[9];
        for (int i = 0; i < 9; i++) {
            buttons[i] = new JButton("");
            buttons[i].setFont(new Font("Arial", Font.BOLD, 60));
            buttons[i].setBackground(new Color(22, 27, 39));
            buttons[i].setForeground(Color.WHITE);
            buttons[i].setFocusPainted(false);
            buttons[i].setBorderPainted(false);
            buttons[i].setCursor(new Cursor(Cursor.HAND_CURSOR));
            buttons[i].addActionListener(this); // Listen for clicks
            boardPanel.add(buttons[i]);
        }

        // ----- 4. Restart button at the bottom -----
        restartButton = new JButton("Restart Game");
        restartButton.setFont(new Font("Poppins", Font.BOLD, 15));
        restartButton.setBackground(new Color(37, 99, 235));
        restartButton.setForeground(Color.WHITE);
        restartButton.setFocusPainted(false);
        restartButton.setBorderPainted(false);
        restartButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
        restartButton.setPreferredSize(new Dimension(180, 40));
        restartButton.addActionListener(e -> resetGame());

        bottomPanel = new JPanel();
        bottomPanel.setBackground(new Color(30, 37, 53));
        bottomPanel.setPadding();  // Note: see below - we use EmptyBorder
        bottomPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 14, 10));
        bottomPanel.add(restartButton);

        // ----- 5. Add all panels to the frame -----
        frame.add(titlePanel,  BorderLayout.NORTH);
        frame.add(boardPanel,  BorderLayout.CENTER);
        frame.add(bottomPanel, BorderLayout.SOUTH);

        frame.setVisible(true); // Make the window appear
    }

    // ============================================================
    // ACTION PERFORMED - Runs every time a grid button is clicked
    // ============================================================
    @Override
    public void actionPerformed(ActionEvent e) {

        // Ignore click if game is already over
        if (gameOver) return;

        // Find out which button was clicked
        JButton clickedButton = (JButton) e.getSource();

        // Ignore click if the cell already has X or O
        if (!clickedButton.getText().equals("")) return;

        // Mark the cell with the current player's symbol
        if (xTurn) {
            clickedButton.setText("X");
            clickedButton.setForeground(new Color(79, 142, 247)); // Blue for X
            statusLabel.setText("Player O's Turn");
        } else {
            clickedButton.setText("O");
            clickedButton.setForeground(new Color(34, 211, 165)); // Green for O
            statusLabel.setText("Player X's Turn");
        }

        // Switch turn
        xTurn = !xTurn;

        // Check if there's a winner
        checkWinner();
    }

    // ============================================================
    // CHECK WINNER - Checks all 8 win combinations after each move
    // ============================================================
    public void checkWinner() {

        for (int[] combo : winCombos) {
            String a = buttons[combo[0]].getText();
            String b = buttons[combo[1]].getText();
            String c = buttons[combo[2]].getText();

            // If all 3 positions are the same non-empty symbol → someone won!
            if (!a.equals("") && a.equals(b) && b.equals(c)) {
                // Highlight the winning cells
                buttons[combo[0]].setBackground(new Color(37, 99, 235));
                buttons[combo[1]].setBackground(new Color(37, 99, 235));
                buttons[combo[2]].setBackground(new Color(37, 99, 235));

                statusLabel.setText("🎉 Player " + a + " Wins!");
                statusLabel.setForeground(new Color(250, 204, 21)); // Yellow for winner
                gameOver = true;
                return;
            }
        }

        // Check for a draw - all 9 cells are filled and no winner
        boolean allFilled = true;
        for (JButton btn : buttons) {
            if (btn.getText().equals("")) {
                allFilled = false;
                break;
            }
        }

        if (allFilled) {
            statusLabel.setText("It's a Draw! Well Played.");
            statusLabel.setForeground(new Color(245, 158, 11)); // Orange for draw
            gameOver = true;
        }
    }

    // ============================================================
    // RESET GAME - Clears the board and starts a fresh game
    // ============================================================
    public void resetGame() {
        for (JButton btn : buttons) {
            btn.setText("");
            btn.setBackground(new Color(22, 27, 39)); // Reset to dark background
        }
        xTurn    = true;
        gameOver = false;
        statusLabel.setText("Player X's Turn");
        statusLabel.setForeground(Color.WHITE);
    }

    // ============================================================
    // MAIN METHOD - Entry point of the program
    // ============================================================
    public static void main(String[] args) {
        // Run on the Event Dispatch Thread (best practice for Swing)
        SwingUtilities.invokeLater(() -> new TicTacToe());
    }
}
If your IDE shows an error on bottomPanel.setPadding(), delete that line - it is handled by setBorder(BorderFactory.createEmptyBorder(...)) on the line right after. The comment was for readability only.
Step 4

Code Explanation - Every Important Concept in Detail

Let's go through every major concept in this program clearly. This section will make sure you understand why each line was written - not just what it does.


4.1 Imports

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
These three imports give us access to all the Swing components and tools we need:

javax.swing.* - Contains JFrame, JButton, JLabel, JPanel, and all visual components.
java.awt.* - Contains layout managers like GridLayout and BorderLayout, plus Color and Font.
java.awt.event.* - Contains ActionListener and ActionEvent, which handle button click events.

The * means "import everything from this package" - a shortcut so we don't have to import each class one by one.

4.2 The Class and ActionListener

public class TicTacToe implements ActionListener
implements ActionListener is a contract. It tells Java: "This class will handle button click events." By implementing ActionListener, we must provide an actionPerformed() method - that method is called automatically whenever any button that has registered this as its listener is clicked.

Analogy: Think of ActionListener as signing up to be "on call". By signing (implementing), you promise to respond when called. The actionPerformed() method is your response.

4.3 Game State Variables

boolean xTurn = true;
boolean gameOver = false;
These two boolean variables act as the game's memory:

xTurn - Tracks whose turn it is. We start with true (X goes first). After every move, we flip it with xTurn = !xTurn. The ! operator means "NOT" - so true becomes false, and false becomes true. Simple and elegant.

gameOver - Once set to true (after a win or draw), the very first line of actionPerformed() checks this and immediately returns - ignoring all further clicks. This prevents players from clicking occupied or post-game cells.

4.4 JFrame Setup (The Window)

frame = new JFrame("Tic Tac Toe | NKDevSpace");
frame.setSize(500, 550);
frame.setLocationRelativeTo(null);
frame.setLayout(new BorderLayout());
new JFrame(...) creates the application window. The string in brackets becomes the window title.

setSize(500, 550) sets the window width to 500 pixels and height to 550 pixels.

setLocationRelativeTo(null) is a very handy trick - passing null tells Java to center the window on the screen. Without this, the window appears in the top-left corner.

new BorderLayout() divides the frame into 5 zones: NORTH (top), SOUTH (bottom), EAST, WEST, and CENTER. We use NORTH for the status label, CENTER for the game grid, and SOUTH for the restart button.

4.5 Creating the Button Grid

boardPanel.setLayout(new GridLayout(3, 3, 6, 6));
buttons = new JButton[9];
for (int i = 0; i < 9; i++) { ... }
GridLayout(3, 3, 6, 6) arranges components in 3 rows and 3 columns, with a 6-pixel horizontal and vertical gap between cells. This automatically creates the game board structure - you just add 9 buttons and GridLayout places them perfectly.

buttons = new JButton[9] creates an array to hold our 9 button objects. Arrays let us loop through all buttons instead of managing 9 separate variables.

Inside the for loop, we create each button, style it, and - most importantly - call buttons[i].addActionListener(this). The keyword this means "the current TicTacToe object". So we're telling each button: "When you are clicked, notify this game object."

4.6 The actionPerformed Method (Click Handler)

public void actionPerformed(ActionEvent e) {
if (gameOver) return;
JButton clickedButton = (JButton) e.getSource();
if (!clickedButton.getText().equals("")) return;
...
}
This method runs every single time any grid button is clicked. Let's break it down line by line:

if (gameOver) return; - If the game has ended, do nothing. The return exits the method immediately.

e.getSource() - The ActionEvent e object contains information about what was clicked. getSource() returns the component that triggered the event. We cast it to JButton to access button-specific methods.

if (!clickedButton.getText().equals("")) - If the button already has text (X or O), it has already been played. We ignore the click.

After these guards, we safely mark the button with the current player's symbol, change its color for visual clarity, update the status label, flip the turn, and call checkWinner().

4.7 Win Detection Logic

for (int[] combo : winCombos) {
String a = buttons[combo[0]].getText();
String b = buttons[combo[1]].getText();
String c = buttons[combo[2]].getText();
if (!a.equals("") && a.equals(b) && b.equals(c)) { ... }
}
winCombos is a 2D array of 8 rows, each containing 3 button indices that form a winning line.

The for-each loop goes through each of the 8 combinations. For each one, we read the text of the 3 buttons at those positions.

The win condition is: !a.equals("") (the cell is not empty) AND a.equals(b) AND b.equals(c) - all three cells have the same symbol. If both conditions are true, the current combo is a winning line.

We then highlight the three winning buttons blue, update the status label with the winner, and set gameOver = true to stop further play.

4.8 Draw Detection

boolean allFilled = true;
for (JButton btn : buttons) {
if (btn.getText().equals("")) { allFilled = false; break; }
}
if (allFilled) { statusLabel.setText("It's a Draw!"); }
Draw detection only runs after we've already confirmed there is no winner. We start by assuming all cells are filled (allFilled = true). We then loop through every button - if we find even one with empty text, we set allFilled = false and break out of the loop early.

If allFilled is still true at the end (meaning all 9 cells have a mark and no winner was found), we declare a draw. The break keyword is an optimization - no need to keep checking once we've found an empty cell.

4.9 The Reset Method

public void resetGame() {
for (JButton btn : buttons) {
btn.setText("");
btn.setBackground(new Color(22, 27, 39));
}
xTurn = true; gameOver = false;
statusLabel.setText("Player X's Turn");
}
The reset method does three things:

1. Loops through all 9 buttons and clears their text back to empty, and restores their original dark background color (removing the blue win highlight if any).

2. Resets xTurn to true (X always starts) and gameOver to false so clicks work again.

3. Updates the status label back to the default "Player X's Turn" message with white text.

The restart button uses a lambda expression (e -> resetGame()) as a shortcut instead of writing a full ActionListener - a modern Java feature that keeps the code clean.

4.10 SwingUtilities.invokeLater

SwingUtilities.invokeLater(() -> new TicTacToe());
Swing applications must be created and updated on a special thread called the Event Dispatch Thread (EDT). Running Swing code on a different thread can cause visual glitches or race conditions.

SwingUtilities.invokeLater() schedules our window creation to run on the EDT. It's the recommended best practice for starting any Swing application - even for small projects. Think of it as "politely asking the GUI thread to start our game when it's ready."
Step 5

Run the Program - Step-by-Step Guide

5.1 Using the Terminal

  1. Open your terminal and navigate to your project folder:
    Terminal
    cd TicTacToeGame
  2. Compile the Java file:
    Terminal
    javac TicTacToe.java
    If there are no errors, you will see a new file called TicTacToe.class appear in the folder. No output means success!
  3. Run the game:
    Terminal
    java TicTacToe

5.2 What You Should See

A 500×550 pixel window will appear in the center of your screen with:

5.3 How to Play

  1. Click any empty cell - it will show X in blue (Player X's move).
  2. The status bar updates to "Player O's Turn".
  3. Click another empty cell - it shows O in green (Player O's move).
  4. Continue alternating until someone gets 3 in a row, column, or diagonal.
  5. The winning 3 cells will turn blue and the status bar will announce the winner in yellow text.
  6. If all 9 cells are filled with no winner, the status bar shows "It's a Draw!"
  7. Click Restart Game at any time to clear the board and start over.

5.4 Common Errors and Fixes

error: class TicTacToe is public, should be in a file named TicTacToe.java
Fix: Make sure your file is saved exactly as TicTacToe.java (capital T, capital T, no spaces). Java is case-sensitive about file names.
'javac' is not recognized as an internal or external command
Fix: Java is not added to your system PATH. Reinstall the JDK and check the "Add to PATH" option, or set JAVA_HOME manually. Close and reopen the terminal after fixing.
error: cannot find symbol - setPadding()
Fix: Delete the bottomPanel.setPadding(); line. Padding is handled by setBorder(BorderFactory.createEmptyBorder(10,10,14,10)) on the next line.
If you are using IntelliJ IDEA, just right-click anywhere in the code and select Run 'TicTacToe.main()'. The game window will appear without needing any terminal commands.
Step 6

Try It Yourself - Challenges to Level Up

You've built a fully working Tic Tac Toe game. Here are some meaningful improvements to push your skills further:

Previous Tutorial Next Tutorial