I'm building a slot game where symbols (divs) are laid out in columns inside a flex container. When a match is detected, I remove the matching elements from the DOM and then new ones fall in from the top.
After the removal, I want the remaining symbols above them to fall smoothly into the empty space (Not the newly generated ones, since they fall in fine). However, since removing elements causes reflow, a transition doesn't apply and the elements just shift downwards to fill the empty spaces that were left by removed elements. And then after that newly generated ones fill whatever is left with a nice transition.
- I tried adding a transition through CSS to the elements that are supposed to fall down.
- I tried setting the height of the removed element to 0 and adding a transition to the height, before it gets removed.
How can I make the symbols fall down with a smooth animation when I remove symbols below it?
This is the logic for the slot, without setting the height to 0, since it looks way worse that way.
const symbols = ["🍒", "🍋", "🍇", "🔔"];const columns = document.querySelectorAll(".col");const spinButton = document.querySelector("#spin-button");const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));const getRandomSymbol = () => { const randomIndex = Math.floor(Math.random() * symbols.length); return symbols[randomIndex];};const generateColumns = () => { [...columns].forEach((col) => { while (col.children.length < 4) { const symbol = getRandomSymbol(); const symbolElement = document.createElement("div"); symbolElement.textContent = symbol; symbolElement.dataset.symbolType = symbol; symbolElement.classList.add("symbol", "above-reel", "falling"); setTimeout(() => { symbolElement.style.transform = "translateY(0%)"; }, 300); col.appendChild(symbolElement); } });};const startSpin = async () => { let currentSymbols = document.querySelectorAll(".symbol"); currentSymbols.forEach((symbol) => { symbol.remove(); }); generateColumns(); await wait(400); checkWins();};const checkWins = async () => { let winningSymbols = []; symbols.forEach((symbol) => { // Check for each symbol let consecutiveCount = 0; for (let col = 0; col < columns.length; col++) { // Check if each column has the symbol const columnSymbols = [...columns[col].children].map( (s) => s.dataset.symbolType ); if (columnSymbols.includes(symbol)) { consecutiveCount++; } else { break; // Stop checking if a column does not have the symbol } } if (consecutiveCount >= 3) { let lastColIndex = consecutiveCount - 1; // Get the last column index where the symbol was found winningSymbols.push([symbol, lastColIndex]); // if there are 3 or more consecutive columns with the same symbol store the winning symbol } }); if (winningSymbols.length !== 0) { await wait(1000); tumble(winningSymbols); } else { console.log("gg"); }};const tumble = async (winningSymbols) => { const allMatches = []; for (let i = 0; i < winningSymbols.length; i++) { for (let j = 0; j < winningSymbols[i][1] + 1; j++) { const matches = columns[j].querySelectorAll( `div[data-symbol-type="${winningSymbols[i][0]}"]` ); allMatches.push(...matches); } } allMatches.map(async (match) => { match.classList.add("removing"); await wait(850); match.remove(); }); await wait(200); generateColumns(); await wait(350); // wait for symbols to drop down before checking checkWins();};spinButton.addEventListener("click", () => { startSpin();});body { font-family: Arial, sans-serif; background: radial-gradient(circle, #000000, #250136); color: white; text-align: center; padding: 20px;}.slots-container { display: flex; justify-content: center; gap: 10px; padding: 20px; background: #222; border-radius: 10px;}.col { display: flex; flex-direction: column-reverse; overflow-y: hidden; gap: 5px; min-height: 215px; width: 60px; background: rgba(255, 255, 255, 0.1); border-radius: 5px; padding: 10px;}.symbol { font-size: 30px; text-align: center; background: white; border-radius: 5px; padding: 5px 0; transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); user-select: none;}.symbol.above-reel { transform: translateY(-500%);}.symbol.removing { animation: fadeOut 0.5s forwards;}@keyframes fadeOut { to { opacity: 0; transform: scale(0); }}/* Controls */.controls { display: flex; justify-content: center; gap: 20px; margin-top: 20px;}#spin-button { padding: 10px 25px; font-weight: bold; background: #f44336; color: white; border: none; border-radius: 5px; cursor: pointer;}#spin-button.spinning { opacity: 0.5; cursor: not-allowed; pointer-events: none;}.box { background: #333; color: white; padding: 10px; border-radius: 5px; min-width: 100px; text-align: center;}<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><link rel="stylesheet" href="style.css" /><title>Document</title><script type="module" src="./js/main.js"></script></head><body><main><div class="slots-container"><div id="col1" class="col"></div><div id="col2" class="col"></div><div id="col3" class="col"></div><div id="col4" class="col"></div><div id="col5" class="col"></div><div id="col6" class="col"></div></div><div class="controls"><div id="last-win" class="box">LAST WIN:</div><button id="spin-button">SPIN</button><div id="balance" class="box"></div></div></main><div id="explosions"></div></body></html>