Technology4 min readLive · checked 12 Oct

CSS view transitions in Laravel and Livewire

How Ball Quiz tiles grow into the game board with one CSS rule. The code, the timings, two demos you can play and the 13 tests it fixed.

Screenshot The Ball Quiz lobby. Each coloured tile grows into its own game. From the 10 October branch.
1CSS rule to opt in
380 mstile-to-board morph
2live demos below
13broken tests fixed
Ball QuizFootball quiz · Laravel and Livewire
#3
LiveChecked live 12 Oct
Visitors
11
Last 24 hours
Revenue
—
Not split by project yet
LaravelLivewirePHP View product Weekly recaps for Ball Quiz →

The short version

  • Ball Quiz pages now animate between each other with one CSS rule.
  • Tap a game tile and it grows into the board. Leave, and it shrinks back.
  • Live on ball-quiz.com as of 12 October. Code, timings and two demos below.

What I built on 10 October

Tap the blue Sprint tile in Ball Quiz and it grows into the game board. Press Leave and it shrinks back.

Ball Quiz is a free football badge quiz I'm building with Laravel and Livewire. The transition itself is plain CSS on top, and it was my favourite commit of the day. This site uses the same feature, view transitions, so I could compare the two.

Live demoTap a tile, then press ×. Same technique on this page; play the real one.

Motion shows what changed

Both projects follow one rule: things move to show what changed.

Ball Quiz: arcade

  • Keys drop onto a hard ledge when pressed
  • A correct answer pops
  • Points count up on the finish card

This site: quiet

  • Pages crossfade when you move between them
  • Nothing bounces
  • Numbers never count up: a business stat shouldn't look like a slot machine

One rule turns page loads into transitions

Ball Quiz pages stay plain documents, and the browser animates the page loads between them.

CSS
@view-transition { navigation: auto; }

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

The browser snapshots the old page, loads the new one and crossfades by default. Browsers without support just load the page.

Leaving is fast (140ms), arriving slower (260ms), with an 8px rise that reads as "new page" without feeling like a slide show:

CSS
::view-transition-old(root) { animation: 140ms cubic-bezier(.4, 0, 1, 1) both bq-vt-page-out; }
::view-transition-new(root) { animation: 260ms cubic-bezier(.16, 1, .3, 1) both bq-vt-page-in; }
@keyframes bq-vt-page-out { to { opacity: 0; } }
@keyframes bq-vt-page-in { from { opacity: 0; transform: translateY(8px); } }

The tile-to-board morph is a shared name

Give an element on each page the same view-transition-name and the browser morphs position and size between them.

A name must be unique on the page at snapshot time and the lobby shows several tiles, so each game gets its own:

CSS
.bq-play-tile[data-game="sprint"],
.bq-game-page[data-game="sprint"][data-game-active="true"] .desk-quiz {
  view-transition-name: bq-game-sprint;
}

::view-transition-group(bq-game-sprint) { animation-duration: 380ms; }

With data-game-active, only a board with a round in progress claims the name, so a setup page can't steal the morph. The daily card and the album band work the same way.

Tile and board differ in shape, so the snapshots crop instead of squash:

CSS
::view-transition-old(*), ::view-transition-new(*) {
  height: 100%; object-fit: cover; overflow: clip;
}

Some things stay still

Deciding what shouldn't move was the fun part.

The header gets its own name and a zero-length animation, so it stays pinned. The active navigation pill gets a name too and slides to the next link over 300ms.

CSS
.desk-header { view-transition-name: bq-header; }
::view-transition-group(bq-header) { animation-duration: 0s; }

Inside the board morph, the bottom-fixed answer sheet would have shrunk into a tile it never belonged to, so it gets its own name and slides down on its own track.

Leave needed JavaScript. From the lobby it goes back through history, which restores the scroll, so the board has a visible tile to shrink into. Before your first answer it skips "Are you sure?", since you haven't lost anything yet.

Feedback inside a round

Answers change state in place: a pop for correct, a short shake for wrong, before you read "Incorrect."

280 msquestion slides in 20px
260 msfeedback sheet slides up
2 sbar, then the next question loads itself
~0.1 skey press

A lost heart pulses once.

CSS
@media (prefers-reduced-motion: no-preference) {
  .desk-quiz-play   { animation: bq-question-in 280ms cubic-bezier(.16, 1, .3, 1) backwards; }
  .bq-game-feedback { animation: bq-sheet-up 260ms cubic-bezier(.16, 1, .3, 1) backwards; }
}
@keyframes bq-question-in { from { opacity: 0; transform: translateX(20px); } }
@keyframes bq-sheet-up    { from { transform: translateY(100%); } }

The bar is an animation on transform: scaleX() with no JavaScript timer, so it can't drift from what the player sees. Key presses are the shortest motion on the site; any slower and the button seems to be thinking about it.

Ball Quiz on a phone after a wrong answer: the chosen tile is red, the correct club is green, and a red bottom sheet says Incorrect with a Next question button.
ScreenshotWrong pick in red, right club in green, and the sheet with Next. Ball Quiz on a phone.

On this site

The same @view-transition rule makes every page change on aimantas.com a short crossfade.

Switching between light and dark uses a same-document transition, so the whole page fades to the new theme instead of flashing. If you want something louder, here's a variant I built as a demo: the new style wipes out in a circle from the button you press. It's a same-document transition plus a clip-path animation:

JavaScript
const r = button.getBoundingClientRect();
root.style.setProperty('--vx', `${r.left + r.width / 2}px`);
root.style.setProperty('--vy', `${r.top + r.height / 2}px`);
root.classList.add('vt-theme');
document.startViewTransition(() => applyStyle(button.dataset.style));
CSS
html.vt-theme::view-transition-old(root) { animation: none; }
html.vt-theme::view-transition-new(root) { animation: wipe .7s cubic-bezier(.65, 0, .35, 1) both; }
@keyframes wipe {
  from { clip-path: circle(0 at var(--vx) var(--vy)); }
  to   { clip-path: circle(150vmax at var(--vx) var(--vy)); }
}

Things move to show what changed.

Press a style. The new one wipes out in a circle from the button you pressed.

Live demoPress a style and watch the circle start from that button. This demo is the only place it runs.

Headlines and the progress bar

A morphing headline needs one extra rule, or it stretches mid-flight.

Snapshots scale like images. object-fit: none; object-position: left top stops that and pins the title top left.

The reading-progress bar uses animation-timeline: scroll(root) with no scroll listener, plus a small JavaScript fallback.

Reduced motion

Both projects listen to prefers-reduced-motion.

Ball Quiz switches view transitions off and drops transforms, confetti, the points pop and the two-second bar, but keeps the text saying the next question is coming. This site keeps the content and drops the movement.

What broke and what's live

On 12 October 2026, ball-quiz.com serves the transitions I committed on 10 October.

Live

The ball-quiz.com stylesheet has both @view-transition rules and names like bq-game-sprint, bq-header and bq-sheet. Each lobby game has its own data-game.

Broke

The previous Ball Quiz commit broke 13 frontend tests. This one fixed them.

Next

Check on real phones, then compare lobby rounds before and after one-tap tiles.

What I'd tell you to try

Start with the opt-in rule: the best cost-to-effect ratio I found.

  1. Add @view-transition { navigation: auto; } and turn it off for reduced motion.
  2. Give a shared element the same unique view-transition-name on both pages. In a list, name each item.
  3. Decide what stays still. A pinned header makes the rest look deliberate.
  4. Make leaving faster than arriving.
  5. Use object-fit on snapshot pseudo-elements when shapes or text would stretch.

Where it stands 12 October 2026

Done

  • Committed 10 October 2026
  • Served on ball-quiz.com (stylesheet checked 12 October)

Not done

  • No screen recording of the live game yet
  • No engagement data

Next

Record the morph on a real phone and compare rounds started before and after.

Receipts

  1. Ball Quiz transition commit: 10 October 2026, including the fix for 13 broken frontend effect tests.
  2. Live check: ball-quiz.com stylesheet fetched 12 October 2026, contains @view-transition and the bq-game-* names. Motion not tested in every browser.

If a fact changes, I add a dated correction here.

Get the weekly recap.

One email a week: the numbers, the newest article and one thing worth trying. The list is open now. The first email goes out once sending is set up end to end.

Unsubscribe anytime. Your email stays private. How I use it

esc

↑ ↓ move↵ open/ search anywhere

Get the weekly recap