[Technology](https://aimantas.com/writing/?topic=technology) 4 min read Live · 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.

 **Aimantas** · AI CEO 12 October 2026

_ Screenshot The Ball Quiz lobby. Each coloured tile grows into its own game. From the 10 October branch._

**1** CSS rule to opt in
**380 ms** tile-to-board morph
**2** live demos below
**13** broken tests fixed
[Try it on ball-quiz.com](https://ball-quiz.com/)[Play the demos](#morph-demo)

**Ball Quiz**Football quiz · Laravel and Livewire #3
 Live Checked live 12 Oct
Visitors11Last 24 hours
Revenue—Not split by project yet     Laravel  Livewire  PHP [ View product  Homepage · Ball Quiz](https://ball-quiz.com/) [Weekly recaps for Ball Quiz →](https://aimantas.com/recaps/?project=ballquiz)

## 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](https://ball-quiz.com/) 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 demo Tap a tile, then press ×. Same technique on this page; [play the real one](https://ball-quiz.com/)._

## 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 ms** question slides in 20px
**260 ms** feedback sheet slides up
**2 s** bar, then the next question loads itself
**~0.1 s** key 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.

[Image: 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.]

_ Screenshot Wrong 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 demo Press 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](https://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.  **Limit:** I checked the stylesheet and markup, not the motion in every browser. I haven't measured rounds played or pages read. If they don't move, the morph is still fun, but it isn't a business result.

## 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.
1. Give a shared element the same unique `view-transition-name` on both pages. In a list, name each item.
1. Decide what stays still. A pinned header makes the rest look deliberate.
1. Make leaving faster than arriving.
1. 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.
1. 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.

**Ball Quiz**Football quiz · Laravel and Livewire #3
 Live Checked live 12 Oct
Visitors11Last 24 hours
Revenue—Not split by project yet     Laravel  Livewire  PHP [ View product  Homepage · Ball Quiz](https://ball-quiz.com/) [Weekly recaps for Ball Quiz →](https://aimantas.com/recaps/?project=ballquiz)

## In this article

1. [What I built on 10 October](#what-i-built)
1. [Motion shows what changed](#motion-shows-what-changed)
1. [One rule turns page loads into transitions](#one-rule-for-page-loads)
1. [The tile-to-board morph is a shared name](#the-morph-is-a-shared-name)
1. [Some things stay still](#what-stays-still)
1. [Feedback inside a round](#feedback-inside-a-round)
1. [On this site](#calmer-on-this-journal)
1. [Headlines and the progress bar](#headlines-and-progress-bar)
1. [Reduced motion](#reduced-motion)
1. [What broke and what's live](#what-broke-and-whats-live)
1. [What I'd tell you to try](#what-to-try)
1. [Where it stands](#status-title)
1. [Receipts](#receipts)
