How To Create A Sliding Puzzle in Webflow

CREATING THE PUZZLE

First, create your image. It should be 3×3 squares. Resulting in 9 boxes. The last needs to be a transparent image. Export and save.

Put in a container and give it a class of ” puzzle-container “

Next drop in the grid, create a 3 x 3 grid. Ensure the cell is the same size as your image square.

Rename the cell class to ” puzzle-piece “

Then in the custom attributes add:

  • Name: data-id
  • Value: 1

Do that for all the cell class of ” puzzle-piece ” but change the value to correspond to the puzzle place, 1 for the 1st piece, 2 for the second piece and so on. HOWEVER, for the last transparent piece. Give it a value of 0.

Then, drop the images in to the cells in the correct order.

Give the image of a class ” puzzle image “

Ensure the images have the alt class “Puzzle Piece 1”, the next “Puzzle Piece 2” and so on.

Add this code in the head

<style>

/* Hide the empty slot */
.puzzle-piece[data-id="0"] {
  display: none;
}

/* Optional: Hover effect */
.puzzle-piece:hover {
  opacity: 0.8;
}

/* Responsive design adjustments */
@media (max-width: 600px) {
  .puzzle-container {
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1; /* Maintain square aspect ratio */
  }
}
</style>

Add this to the body. NOTE: Where I put “URL OF IMAGE 1” etc put the url of the actual image into it.

<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
  const gridSize = 3; // 3x3 grid
  const totalPieces = gridSize * gridSize;

  const puzzleContainer = document.querySelector('.puzzle-container');
  const pieces = Array.from(document.querySelectorAll('.puzzle-piece'));
  const congratsMessage = document.querySelector('.congrats-message');
  const restartButton = document.getElementById('restart-button');
  const closeButton = document.getElementById('close-button');
  const moveCountElement = document.querySelector('.move-count');
  const timerCountElement = document.querySelector('.timer-count'); // Select the timer-count element

  // Map piece numbers to image URLs
  const imageSources = {
    1: 'URL OF IMAGE 1',
    2: 'URL OF IMAGE 2',
    3: 'URL OF IMAGE 3',
    4: 'URL OF IMAGE 4',
    5: 'URL OF IMAGE 5',
    6: 'URL OF IMAGE 6',
    7: 'URL OF IMAGE 7',
    8: 'URL OF IMAGE 8',
  };

  let positions = [];
  let moveCount = 0; // Initialize move count
  let timerInterval = null;
  let elapsedTime = 0; // Time in seconds
  let isTimerStarted = false; // Flag to track if timer has started

  // Function to update the move count display
  function updateMoveCount() {
    if (moveCountElement) {
      moveCountElement.textContent = `Moves: ${moveCount}`;
      console.log(`Move count updated: ${moveCount}`);
    } else {
      console.error('Move count element not found. Ensure there is a div with class "move-count".');
    }
  }

  // Function to update the timer display
  function updateTimerCount() {
    if (timerCountElement) {
      timerCountElement.textContent = `Time: ${elapsedTime}s`;
      console.log(`Timer updated: ${elapsedTime}s`);
    } else {
      console.error('Timer count element not found. Ensure there is a div with class "timer-count".');
    }
  }

  // Function to start the timer
  function startTimer() {
    // Prevent multiple timers
    if (isTimerStarted) return;

    isTimerStarted = true;
    console.log('Timer started.');

    // Start a new timer
    timerInterval = setInterval(() => {
      elapsedTime++;
      updateTimerCount();
    }, 1000); // Update every second
  }

  // Function to stop the timer
  function stopTimer() {
    if (timerInterval) {
      clearInterval(timerInterval);
      timerInterval = null;
      console.log('Timer stopped.');
    }
  }

  // Function to initialize the puzzle
  function initPuzzle() {
    // Initialize positions array with IDs from 1 to 8 and 0 for empty slot
    positions = [];
    for (let i = 0; i < totalPieces; i++) {
      positions.push(i === totalPieces - 1 ? 0 : i + 1); // [1,2,3,4,5,6,7,8,0]
    }

    // Shuffle positions to start the game
    positions = shuffle(positions.slice());

    // Ensure that there is exactly one empty slot
    if (positions.filter(id => id === 0).length !== 1) {
      console.error('There must be exactly one empty slot (data-id="0").');
      return;
    }

    // Map shuffled positions to the puzzle pieces
    pieces.forEach((piece, index) => {
      const posIndex = positions[index];
      piece.dataset.position = index.toString(); // Current grid position

      if (posIndex === 0) {
        // Empty slot
        piece.dataset.id = '0';
        // Use a transparent image for the empty slot
        const img = piece.querySelector('img');
        if (img) {
          img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; // 1x1 transparent pixel
          img.alt = '';
        } else {
          console.error(`Image element not found in piece with data-id 0`);
        }
      } else {
        piece.dataset.id = posIndex.toString();

        // Update the image source based on the shuffled position
        const img = piece.querySelector('img');
        if (img) {
          img.src = imageSources[posIndex];
          img.alt = `Puzzle Piece ${posIndex}`;
        } else {
          console.error(`Image element not found in piece with data-id ${posIndex}`);
        }
      }

      // Set the grid position
      setPosition(piece, index);
    });

    // Hide the congratulatory message if it's visible
    if (congratsMessage) {
      congratsMessage.style.display = 'none';
    } else {
      console.error('Congratulatory message div not found. Ensure there is a div with class "congrats-message".');
    }

    // Reset move count and timer
    moveCount = 0;
    updateMoveCount();
    stopTimer();
    isTimerStarted = false;
    elapsedTime = 0;
    updateTimerCount();

    console.log('Puzzle initialized with positions:', positions);
  }

  // Initialize the puzzle
  initPuzzle();

  // Add click event listeners to the pieces
  pieces.forEach(piece => {
    piece.addEventListener('click', () => {
      movePiece(piece);
    });
  });

  // Event listener for the restart button
  if (restartButton) {
    restartButton.addEventListener('click', () => {
      initPuzzle();
    });
  } else {
    console.error('Restart button not found. Ensure there is a button with ID "restart-button".');
  }

  // Event listener for the close button
  if (closeButton) {
    closeButton.addEventListener('click', () => {
      if (congratsMessage) {
        congratsMessage.style.display = 'none';
        console.log('Congratulatory message closed.');
      } else {
        console.error('Congratulatory message div not found.');
      }
    });
  } else {
    console.error('Close button not found. Ensure there is a button with ID "close-button".');
  }

  // Function to set the position of a piece in the grid
  function setPosition(piece, positionIndex) {
    piece.style.gridColumnStart = (positionIndex % gridSize) + 1;
    piece.style.gridRowStart = Math.floor(positionIndex / gridSize) + 1;
    piece.dataset.position = positionIndex.toString();

    // Show the piece
    piece.style.display = '';
  }

  // Function to move a piece if it's adjacent to the empty slot
  function movePiece(piece) {
    const emptyPiece = pieces.find(p => p.dataset.id === '0');
    if (!emptyPiece) {
      console.error('Empty piece not found. Ensure there is a puzzle-piece with data-id="0".');
      return;
    }

    const emptyIndex = parseInt(emptyPiece.dataset.position);
    const pieceIndex = parseInt(piece.dataset.position);

    console.log(`Attempting to move piece ${piece.dataset.id} from position ${pieceIndex} to empty position ${emptyIndex}`);

    const isAdjacent = checkAdjacent(pieceIndex, emptyIndex);
    if (isAdjacent) {
      // Swap positions
      swapPieces(piece, emptyPiece);

      // Increment move count
      moveCount++;
      updateMoveCount();

      // Start the timer on the first move
      if (!isTimerStarted) {
        startTimer();
      }

      // Check if the puzzle is solved
      if (checkWin()) {
        stopTimer(); // Stop the timer when the puzzle is solved
        showCongratsMessage();
      }
    } else {
      console.log(`Piece ${piece.dataset.id} is not adjacent to the empty slot.`);
    }
  }

  // Function to swap two pieces
  function swapPieces(piece1, piece2) {
    const pos1 = parseInt(piece1.dataset.position);
    const pos2 = parseInt(piece2.dataset.position);

    // Swap positions in the positions array
    [positions[pos1], positions[pos2]] = [positions[pos2], positions[pos1]];

    console.log(`Swapped piece ${piece1.dataset.id} (position ${pos1}) with piece ${piece2.dataset.id} (position ${pos2})`);

    // Update dataset positions
    piece1.dataset.position = pos2.toString();
    piece2.dataset.position = pos1.toString();

    // Update positions in the DOM
    setPosition(piece1, pos2);
    setPosition(piece2, pos1);
  }

  // Function to check if two indices are adjacent in the grid
  function checkAdjacent(index1, index2) {
    const row1 = Math.floor(index1 / gridSize);
    const col1 = index1 % gridSize;
    const row2 = Math.floor(index2 / gridSize);
    const col2 = index2 % gridSize;

    // Check if the positions are next to each other
    const isAdjacent = (Math.abs(row1 - row2) + Math.abs(col1 - col2)) === 1;
    console.log(`Checking adjacency between positions ${index1} and ${index2}: ${isAdjacent}`);
    return isAdjacent;
  }

  // Function to check if the puzzle is solved
  function checkWin() {
    const correctOrder = [1, 2, 3, 4, 5, 6, 7, 8, 0];
    for (let i = 0; i < positions.length; i++) {
      if (positions[i] !== correctOrder[i]) {
        return false;
      }
    }
    console.log('Puzzle solved!');
    return true;
  }

  // Function to show the congratulatory message
  function showCongratsMessage() {
    if (congratsMessage) {
      congratsMessage.style.display = 'flex'; // Use 'flex' to match the CSS styling
      console.log('Congratulatory message displayed.');
    } else {
      console.error('Congratulatory message div not found. Ensure there is a div with class "congrats-message".');
    }
  }

  // Function to shuffle the positions array
  function shuffle(array) {
    let currentIndex = array.length, randomIndex;

    // While there remain elements to shuffle
    while (currentIndex !== 0) {
      randomIndex = Math.floor(Math.random() * currentIndex);
      currentIndex--;

      // Swap elements
      [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
    }

    // Ensure the puzzle is solvable
    if (!isSolvable(array)) {
      // Swap two tiles (excluding the empty slot) to make it solvable
      if (array[0] !== 0 && array[1] !== 0) {
        [array[0], array[1]] = [array[1], array[0]];
        console.log('Shuffled array was unsolvable. Swapped first two tiles to make it solvable.');
      } else {
        [array[1], array[2]] = [array[2], array[1]];
        console.log('Shuffled array was unsolvable. Swapped second and third tiles to make it solvable.');
      }
    }

    console.log('Shuffled positions:', array);
    return array;
  }

  // Function to check if the shuffled puzzle is solvable
  function isSolvable(array) {
    let inversions = 0;
    for (let i = 0; i < array.length; i++) {
      for (let j = i + 1; j < array.length; j++) {
        if (array[i] && array[j] && array[i] > array[j]) {
          inversions++;
        }
      }
    }

    const emptyRow = gridSize - Math.floor(array.indexOf(0) / gridSize);

    if (gridSize % 2 !== 0) {
      // Odd grid size, solvable if inversions are even
      const solvable = inversions % 2 === 0;
      console.log(`Odd grid size. Inversions: ${inversions}. Solvable: ${solvable}`);
      return solvable;
    } else {
      // Even grid size
      if (emptyRow % 2 === 0) {
        const solvable = inversions % 2 !== 0;
        console.log(`Even grid size. Empty row from bottom: ${emptyRow}. Inversions: ${inversions}. Solvable: ${solvable}`);
        return solvable;
      } else {
        const solvable = inversions % 2 === 0;
        console.log(`Even grid size. Empty row from bottom: ${emptyRow}. Inversions: ${inversions}. Solvable: ${solvable}`);
        return solvable;
      }
    }
  }
});
</script>

CONGRATULATIONS MESSAGE

I also added the function to give a congratulations message and to restart the puzzle or not.

Create a Div with the class “congrats-message”

Throw another Div within it and class the class “congrats-content”

Put your message in there.

Then create a button for restart, and give it an ID (which can be found in the settings) of “restart-button”

Then create another button but with the ID “close-button”

For me, I made it so it is an absolute div that will fill up the whole screen. You can design it as you want.

Make sure it is hidden when you are done. You can do this by going to Layout>Display>None

ADDING STATS

I also included stats for the puzzle, which are a timer and number of moves.

Timer

Create a div with the class “timer”, then throw a paragraph within it, give the class “timer-count” for the paragraph. Throw in dummy text, like timer.

Moves

Create a div with the class “move-counter”, then throw a paragraph within it, give it the class “move-count” for the paragraph. Throw in dummy text, like move count.

And that should be it!