Skip to article body
A woman in a blue dress walking into a narrow Cairo alley lit by a single lamp
A woman in a blue dress walking into a narrow Cairo alley lit by a single lamp

Web Development · Field Notes

The Mobile Branch That Never Ran

A GSAP matchMedia call with a ternary inside, and why phones skipped both sides of it.

By Shehab Waleed, Founder, Cairo Studio 3 min read

The process section on our studio page is a horizontal track of steps. On desktop you scroll through it. On a phone you swipe, and a progress bar fills as you go. For three weeks after launch, phones got none of that. Nothing moved, nothing threw an error, and desktop worked, so nobody noticed.

The code looked complete

We set the animation up with gsap.matchMedia(), using the conditions form so one function can handle both layouts.

mm.add(
{
isDesktop: '(min-width: 1025px)',
reduceMotion: '(prefers-reduced-motion: reduce)'
},
(ctx) =>
ctx.conditions.isDesktop
? desktop(ctx.conditions.reduceMotion)
: mobile(ctx.conditions.reduceMotion)
);

Read top to bottom, it covers every case. Desktop gets one function and everything else gets the other. That reading assumes the callback always runs.

It runs only when something matches

GSAP calls a conditions callback when at least one query in the object matches, and again whenever the set of matches changes. On a phone 390 pixels wide, with reduced motion off, isDesktop is false and reduceMotion is false. Nothing matches, so the function is never called and the ternary never gets the chance to pick mobile().

There was no error to find. The section still rendered, and the desktop version kept working on the screens we built it on.

One line

Every branch inside the callback needs a query that is true when that branch should run. We added one for phones.

{
isDesktop: '(min-width: 1025px)',
isMobile: '(max-width: 1024px)',
reduceMotion: '(prefers-reduced-motion: reduce)'
}

We checked both versions in headless Chromium before writing this. At 390 pixels wide, the old object never called the function, and the new one called it and chose mobile. At 1440 pixels both chose desktop.

Why it hid so well

Three things kept it out of sight. We develop on wide screens, where the code works. The fallback for no animation is a layout that looks deliberate. And a phone with reduced motion switched on makes reduceMotion match, so the callback does run there, and the bug only shows on the phones where motion is allowed.

How we check phone animation now

Desktop is easy to judge by eye. For phones we load the page in WebKit with an iPhone viewport and look for the inline styles GSAP writes. If an element that should animate has nothing written on it after load, the setup did not run. A check like this one is enough:

const ran = await page.evaluate(() =>
[...document.querySelectorAll('.step')]
.some((el) => el.style.transform !== '')
);

Touch needs one more step. Simulated touch through safaridriver did not reach the page in the iOS Simulator for us, so our tests dispatch pointer events with pointerType set to touch from inside the page.

Phones were also where our scroll linked rows fell behind the finger. It is a different bug with the same lesson: check phones on their own terms.

Share
X LinkedIn Facebook WhatsApp Email
A hand on a half open blue door at the top of a dark stairwell

Written, and never opened.

Old conditions
Runs desktop()
With isMobile
Runs desktop()
Old conditions
Runs desktop(true)
With isMobile
Runs desktop(true)
Old conditions
Never called
With isMobile
Runs mobile()
Old conditions
Runs mobile(true)
With isMobile
Runs mobile(true)

The rows with motion allowed were run in headless Chromium. The reduced motion rows follow from the same rule.

With a conditions object, GSAP calls the function only when at least one query matches. Check whether the current screen and settings match any of them.

Yes. A ternary on ctx.conditions only works once the callback is called, so each side needs a query that is true when that side applies.

It is easier to read: one query, one function, and GSAP reverts it when the query stops matching. We use the conditions form when two layouts share most of their setup.

We design and build fast, animated sites, and we test them on real phones before they ship.

See our web development work

More from the Journal

view all →