I had analytics data for weeks showing mobile underperformance. I never noticed because I was looking at the wrong number.
I run RepoClip, a SaaS that generates promo videos from GitHub repos. The pipeline analyzes code with Gemini, generates images/video clips, adds narration, and renders a video. The previous articles covered image model migration and video clip integration.
This time, the problem wasn't in the AI pipeline. It was in a CSS grid.
The Aggregate Lie
My GA4 dashboard showed an overall bounce rate that looked fine. Mobile users were 28% of traffic — a meaningful chunk. But the aggregate number hid the real story.
When I broke it down by page using BigQuery, the conversion funnel on /dashboard/new (the video creation page) told a different story:
29 page views → 5 video generations
That's a 17% conversion rate on the page where users actually create videos. Desktop was converting at roughly 3x that rate.
What Was Actually Broken
I opened Chrome DevTools, switched to iPhone SE (375px), and immediately saw the problems.
Problem 1: Grids That Don't Fit
The content mode selector uses a 3-column grid. On 375px, each button was ~105px wide — barely enough for the emoji, label, and "100 credits" text. The visual style selector was worse: 4 columns on a phone screen.
// Before: cramped on mobile
<div className="grid grid-cols-3 gap-3">
{CONTENT_MODES.map((cm) => (
<button className="flex flex-col items-center gap-1.5 p-3 ...">
<span className="text-xl">{cm.emoji}</span>
<span className="text-xs font-medium">{cm.label}</span>
<span className="text-[10px]">{cm.description}</span>
<span className="text-[10px]">{cm.credits} credits</span>
</button>
))}
</div>
The fix was one class change per grid:
- <div className="grid grid-cols-3 gap-3">
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
- <div className="grid grid-cols-4 gap-3">
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
Two columns on mobile, full grid on desktop. The buttons went from cramped and barely tappable to comfortable with clear labels.
Problem 2: The URL Input Squeeze
The URL input and "Validate" button sat side by side in a flex row. On a phone, the input was too narrow to show the full URL — users couldn't see what they'd typed:
- <div className="flex gap-3">
+ <div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
Stacking vertically on mobile gave the input full width. Simple, obvious in hindsight.
Problem 3: The Real Friction — Typing a URL on a Phone
This was the insight that went beyond CSS.
On desktop, users copy a GitHub URL from their browser and paste it. One action. On mobile, users have to:
- Switch to their browser
- Navigate to GitHub
- Find the repo
- Copy the URL
- Switch back to RepoClip
- Paste it
- Tap "Validate"
That's 7 steps before they even start configuring their video. No amount of responsive CSS fixes this.
The One-Tap Solution
RepoClip already has users' GitHub OAuth tokens (for private repo access). Those tokens can fetch their recent repositories:
// New GET handler in /api/github
export async function GET() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ repos: [] });
const { data: profile } = await supabase
.from("profiles")
.select("github_access_token")
.eq("id", user.id)
.single();
const token = profile?.github_access_token;
if (!token) return NextResponse.json({ repos: [] });
const octokit = new Octokit({ auth: token });
const { data: repos } = await octokit.repos.listForAuthenticatedUser({
sort: "updated",
per_page: 5,
});
return NextResponse.json({
repos: repos.map((r) => ({
owner: r.owner.login,
name: r.name,
description: r.description,
stars: r.stargazers_count,
language: r.language,
isPrivate: r.private,
})),
});
}
On the frontend, when the URL input is empty, the user sees their 5 most recent repos as tappable chips:
{!githubUrl && !repoInfo && recentRepos.length > 0 && (
<div>
<p className="text-xs text-slate-500 mb-2">Recent repositories</p>
<div className="flex flex-wrap gap-2">
{recentRepos.map((repo) => (
<button
key={`${repo.owner}/${repo.name}`}
onClick={() => {
const url = `https://github.com/${repo.owner}/${repo.name}`;
setGithubUrl(url);
// Auto-validate immediately
validateRepo(url);
}}
className="px-3 py-1.5 text-xs font-medium rounded-full border ..."
>
{repo.owner}/{repo.name}
</button>
))}
</div>
</div>
)}
Tap a chip, auto-validate, start configuring. Three taps instead of 7 steps. On desktop, it's a nice convenience. On mobile, it eliminates the primary friction point.
The Analytics Blind Spot
While investigating the mobile conversion gap, I tried to answer a natural question: do mobile users choose portrait (9:16) video and desktop users choose landscape (16:9)?
I couldn't answer it. The video_generate_start GA4 event didn't include aspect_ratio:
// Before: missing aspect_ratio and visual_style
event("video_generate_start", {
repo_owner: repoInfo?.owner ?? "",
repo_name: repoInfo?.name ?? "",
content_mode: contentMode,
bgm_enabled: bgmEnabled ? "true" : "false",
});
I could see aspect ratio distribution in Supabase (82% landscape, 13% portrait, 5% square) and device breakdown in GA4 (83% desktop, 17% mobile for generations). But I couldn't correlate them — different systems, different user IDs.
The fix was two lines:
event("video_generate_start", {
repo_owner: repoInfo?.owner ?? "",
repo_name: repoInfo?.name ?? "",
content_mode: contentMode,
+ aspect_ratio: aspectRatio,
+ visual_style: visualStyle,
bgm_enabled: bgmEnabled ? "true" : "false",
});
Now I can run this BigQuery query:
SELECT
device.category,
(SELECT value.string_value
FROM UNNEST(event_params)
WHERE key = 'aspect_ratio') AS aspect_ratio,
COUNT(*) AS generates
FROM `analytics.events_*`
WHERE event_name = 'video_generate_start'
GROUP BY device.category, aspect_ratio
ORDER BY generates DESC
The data won't be available until the next deployment collects new events, but the hypothesis is testable now. If mobile users do prefer portrait videos, I should default to 9:16 when navigator.userAgent indicates a mobile device.
The Landing Page Badge Overflow
One more mobile issue, unrelated to conversions but visible to every visitor: the third-party badges (TAAFT, BetaList, Futurepedia) on the landing page had hard-coded pixel widths.
// Before: fixed widths overflow on 375px screens
<div className="flex justify-center items-center gap-6 py-8">
<img width="300" src="..." />
<img width="156" height="54" style={{ width: 156, height: 54 }} />
<img width={250} height={54} style={{ width: 250, height: 54 }} />
</div>
300 + 156 + 250 + gaps = 730px minimum. iPhone SE is 375px. The badges just overflowed.
// After: responsive widths with flex-wrap
<div className="flex flex-wrap justify-center items-center gap-4 sm:gap-6 px-4 py-8">
<img src="..." className="w-48 sm:w-[300px]" />
<img src="..." className="h-10 sm:h-[54px] w-auto" />
<img src="..." className="h-10 sm:h-[54px] w-auto" />
</div>
flex-wrap lets badges flow to a second row on small screens. Responsive height classes scale them down proportionally.
What I Learned
1. Aggregate metrics hide page-level problems. "Mobile bounce rate" told me nothing. Breaking conversion down by page and device category revealed that one specific page was broken. If you have GA4 + BigQuery, query by page path — not just by device.
2. The biggest mobile friction isn't layout — it's input. Responsive grids are table stakes. The real question is: what actions require typing on mobile that could be replaced with tapping? For RepoClip, the answer was URL input. For your app, it might be search, filters, or form fields.
3. Track what you'll want to correlate later. I added content_mode to the GA4 event at launch but forgot aspect_ratio and visual_style. When I needed to correlate device type with aspect ratio choice, the data wasn't there. Think about the questions you'll ask in 3 months and instrument for them now.
4. Fixed pixel widths are mobile time bombs. Every width="300" and style={{ width: 250 }} is a mobile overflow waiting to happen. Use responsive classes from the start, even if your primary audience is desktop.
The Checklist
If you're building a SaaS and haven't checked your mobile conversion funnel recently, here's my quick audit list:
- [ ] Query GA4 by page path + device category, not just aggregate
- [ ] Open every conversion-critical page on a 375px viewport
- [ ] Check every grid: does it still make sense at half the width?
- [ ] Find every input that requires typing — can it be replaced with selection?
- [ ] Search your codebase for hard-coded
width=attributes - [ ] Verify your analytics events include all dimensions you'll want to slice by
Try It
The mobile improvements are live now: repoclip.io
If you have a GitHub repo, try the one-tap flow: log in, go to "Create New Video," and your recent repos should appear as chips. On a phone, it's noticeably faster than typing a URL.
Questions for the community:
- What's the worst mobile UX bug you've found hiding in aggregate analytics?
- How do you decide when to optimize for mobile vs. accepting that some workflows are desktop-first?
Drop a comment or find me on GitHub.
Four CSS changes, one API endpoint, and two GA4 parameters. Sometimes the highest-impact work isn't in the AI pipeline — it's in the form that feeds it.
Top comments (0)