HTML6 GamesFree Web Games / devlog

No install. Just play.

Free mini games right in your browser. Sign in to save your records to your account and pick up on any device.

All games
All games
All dev log posts

The board flashed on iPhone every time a card moved

Nov 1, 2026 Β· Tech Β· ν•œκ΅­μ–΄

Playing Solitaire on an iPhone, when I dragged and dropped a card, the whole board flashed as the card flew into place. The board went blank for a split second and then came back. The same day, the same thing happened in Spider Solitaire when dealing from the stock, and in FreeCell when moving a card. It never showed up once in desktop Chrome.

These card games are drawn with HTML elements, not a canvas. Each card is one <div>, and the columns and empty slots are elements too. The flying-card animation uses a technique called FLIP. Before the move, measure each card's position on screen (First). Redraw the board in its new state and measure the new positions (Last). Shift each card back by the difference so it sits in its old spot (Invert), then animate it to where it belongs (Play). Layout changes only once and the motion is all transform, which is why it's a common technique.

Copying a fix that worked in another game

Just before that, I had fixed a similar symptom in Tile Pairs. The board flickered at the end of the shuffle animation, and the cause was the z-index raised during the animation to lift flying tiles above the others, then reset when the animation finished. Safari draws the screen as a few separate layers, and I took it that a change in stacking order made it split the layers again and redraw the whole board. So I stopped resetting z-index and gave moving tiles will-change: transform so they always get their own layer. That fixed Tile Pairs.

The card games looked like the same symptom. I put the same fix into all three. But the cause was a guess. It only happened on iPhone, so I couldn't reproduce it on the Mac I develop on, and all I could check on desktop was that the animation still ran.

Trying again on a real iPhone, all three games still flashed.

The whole board was being rebuilt

Looking at the code again, the three games had the same kind of draw function. On every single card move, it built the HTML for the whole board as a string and assigned it to innerHTML. Solitaire's 52 card elements and Spider's 104 were deleted and created anew each time. And the FLIP animation was applied straight to those brand-new elements. Simplified, it looked like this:

// before: every move rebuilt the whole board
function draw() {
  board.innerHTML = columns.map((col) =>
    `<div class="col">${col.map((c) => `<div class="card" data-id="${c.id}">…</div>`).join('')}</div>`
  ).join('');
  animateFrom(before); // FLIP on brand-new elements
}

The first fix piled on top of this. With will-change: transform on every card, dozens of freshly created cards each asked for their own layer at once. While Safari built and painted those layers, the board looked empty. Desktop Chrome seems to have finished that within one frame, which is why it never showed. The will-change that worked in Tile Pairs made things worse here.

The fix was to stop recreating elements. The board frame (columns, empty slots, stock) is built once when the game opens. Card elements are created once per card id, kept in a Map and reused. Drawing moves each card element into its column and switches state, such as face up or face down, with classes. A column's children are replaced with replaceChildren only when the list actually changed. If the same element is already in the same place, nothing is touched.

const cardEls = new Map(); // card id -> element, created once
function cardEl(card) {
  let el = cardEls.get(card.id);
  if (!el) {
    el = document.createElement('div');
    el.dataset.id = card.id;
    cardEls.set(card.id, el);
  }
  el.className = 'card' + (card.up ? ' up' : '');
  return el;
}

function put(column, kids) { // touch the DOM only when the order really changed
  const same = kids.length === column.children.length &&
    kids.every((k, i) => column.children[i] === k);
  if (!same) column.replaceChildren(...kids);
}

Passing elements that are already on the page to replaceChildren moves them instead of creating new ones. Moving one card changes only two columns, and the other columns and cards stay as they are. I removed the per-card will-change.

Reusing elements brought new things to take care of. Before, every element was new, so no earlier animation could linger on it. Now it's the same element, so if you make the next move while a card is still flying, its animation is still running. So before redrawing, the game measures where the card currently appears, stops the running animation and flies it again from there. The position set while dragging and the dotted drop-target outline used to vanish with the old elements; now the draw function clears them itself.

After these changes I played the three games again on a real iPhone. This time the board didn't flash when cards moved.

Three days later a report came in that the board flickered when tapping tiles in Number Slide. It turned out to rebuild the board with innerHTML on every slide. This time I went straight to keeping the tile elements and changing only their numbers and pictures. It's now part of the rules I follow when building games: don't rebuild the whole board on each move, and don't keep will-change on lots of elements.

The same symptom was no guarantee of the same cause. Tile Pairs never recreated its tiles; the card games recreated everything every time. If I had compared how the two games drew first, I could have skipped the first fix.

References

Games from this post

SolitaireKlondike solitaire. Build all four suits from Ace to King. Play Spider SolitaireBuild 8 runs from King down to Ace in one suit across 10 columns. Play FreeCellSolitaire with every card face up. Use the four free cells wisely to build each suit from Ace to King. Play

More posts

Add to Home Screen

  1. Tap the Share button at the bottom of the screen (top on iPad). It looks like a square with an arrow pointing up.
  2. Scroll down and tap "Add to Home Screen".
  3. Tap "Add" at the top right. The HTML6 Games icon appears on your home screen.

This works in Safari. Opened from the home screen, it runs without the address bar, like an app, and even offline.