Mobile-First Playground: Making an Astrology Grid Actually Work on a Phone (And Go Viral While Doing It)
The Destiny Grid looked great on desktop. On a 375px phone screen, it was an unscrollable, untappable, unshareable mess. Here's how I rebuilt the entire playground experience for mobile — touch targets, momentum scrolling, Web Share API, screenshot-friendly cards, and the surprisingly tricky art of making a 100-cell grid feel good under a thumb.
Series: VIBE.LOG
- 1. The Layout Vocabulary Cheat Sheet: What to Call That Thing on Your Screen
- 2. I Spent 3 Hours Trying to Proxy a Blog Subdomain. Here's My Descent Into Madness.
- 3. The Complete SEO Guide: How to Make Google Actually Notice Your Website
- 4. Why Your Next.js Favicon Isn't Showing (And the Three Ways to Actually Fix It)
- 5. GitHub Keeps Telling Me My Branch Is Fine. And Also Not Fine. At the Same Time.
- 6. Mobile-First Playground: Making an Astrology Grid Actually Work on a Phone (And Go Viral While Doing It) ← you are here
- 7. Playground Is Live: The Destiny Grid, Real Astrology, and Why I'm Shipping a Toy Every Month
- 8. The Interactive Component Cheat Sheet: What to Call That Clickable Thing
- 9. Google Rejected My Site for 'Low-Value Content.' Here's What I Actually Fixed.
- 10. I Actually Fixed Everything. Here's What That Looked Like.
- 11. I Hired 131 AI Employees Today. Here's How.
- 12. I Let My AI Run 72 Backtests While I Watched. It Picked the Winner.
- 13. I Taught My AI to Stop Asking Questions. It Took Five Rewrites.
- 14. Obsidian Turned My Scattered Notes Into a Second Brain. Here's How to Set It Up.
- 15. The Destiny Grid Gets Its East Wing: I Rebuilt Saju (四柱八字) in TypeScript
- 16. Molecule Me: Your Personality, Encoded in Chemistry
The Destiny Grid looked great on desktop. A 10-column color grid, smooth hover tooltips, a crisp SVG fortune chart — every year of your life mapped to a single beautiful screen.
Then I opened it on my phone.
The grid cells were 17 pixels wide. The "Reveal My Grid" button was partially hidden behind the birthplace dropdown. The tooltip floated off the right edge of the screen. The share button copied a URL to the clipboard with no feedback on iOS Safari. The SVG chart was completely unresponsive to touch.
It was, in technical terms, a disaster. And since /playground is designed to be the viral entry point — something people find through a shared link on Twitter or KakaoTalk — this wasn't just a cosmetic problem. It was a growth problem.
This post documents everything I changed to make the playground feel native on mobile, and the specific decisions behind each fix.
🎯 The Core Problem: Desktop Assumptions Baked Into Every Layer
The original implementation wasn't anti-mobile. It used clamp() for fluid typography. It had flex-wrap on the input row. The preview grid hid itself on small screens with hidden sm:flex. These are reasonable responsive patterns.
But responsive CSS alone doesn't make something mobile-friendly. Here's what was actually broken:
The left side is how the desktop version works: comfortable 46px grid cells, mouse-hover tooltips, a single "Share My Destiny" button. The right side shows the mobile target: stacked full-width inputs, touch-responsive grid, platform share buttons (X, Facebook, Kakao), and a screenshot-friendly share card at the bottom. Two completely different interaction models, same underlying data.
✅ Fix 1: The Input Stack — Stop Pretending Flex-Wrap Is Enough
The original input row was a display: flex with flex-wrap: wrap and gap: 2rem. On desktop, this puts all four inputs (birthdate, city, life expectancy slider, reveal button) in a single comfortable row. On a 375px screen, the items wrap into a jumbled mess where the button ends up orphaned below the slider with inconsistent spacing.
The fix was switching to a proper CSS Grid that adapts based on screen width:
// Before: flex-wrap prayer
<div style={{ display: "flex", flexWrap: "wrap", gap: "2rem", alignItems: "flex-end" }}>
// After: explicit mobile grid
<div style={{
display: "grid",
gridTemplateColumns: isMobile ? "1fr" : "auto auto auto auto",
gap: isMobile ? "1.25rem" : "2rem",
alignItems: "end",
}}>On mobile, every input gets full width. The birthdate field spans the entire screen. The city dropdown is large enough that you don't accidentally tap the wrong option. The "Reveal My Grid" button stretches edge-to-edge with a minHeight: 48px — Apple's Human Interface Guidelines recommend at least 44px for touch targets, and I went slightly larger because fat thumbs exist.
The isMobile flag comes from a simple hook:
function useIsMobile() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const check = () => setIsMobile(window.innerWidth < 640);
check();
window.addEventListener("resize", check);
return () => window.removeEventListener("resize", check);
}, []);
return isMobile;
}Why not use CSS media queries for all of this? Because the grid section, the tooltip positioning, and the share logic all need runtime knowledge of the viewport. A CSS-only approach would require duplicating the entire grid component. The hook gives us a single decision point that cascades through the entire component.
✅ Fix 2: The Grid Cells — From 17px to Tappable
This was the most critical fix. The original cell sizing used clamp(28px, 4.5vw, 46px). On a desktop at 1440px, 4.5vw = ~65px, clamped to 46px. Perfect. On a phone at 375px, 4.5vw = ~17px. That's smaller than most people's fingertip. You literally cannot tap a single cell without hitting its neighbors.
// Before: desktop clamp, mobile disaster
width: "clamp(28px, 4.5vw, 46px)",
height: "clamp(28px, 4.5vw, 46px)",
// After: mobile-aware sizing
width: isMobile ? "clamp(30px, 8vw, 38px)" : "clamp(28px, 4.5vw, 46px)",
height: isMobile ? "clamp(30px, 8vw, 38px)" : "clamp(28px, 4.5vw, 46px)",On a 375px screen, 8vw = 30px, which hits the minimum. On a 414px iPhone Plus, 8vw = 33px. Both are within the comfortable touch zone. The grid now overflows horizontally with momentum scrolling:
<div className="scroll-momentum" style={{
overflowX: "auto",
WebkitOverflowScrolling: "touch",
paddingBottom: "0.5rem",
}}>The scroll-momentum class in the global CSS provides the iOS rubber-band scrolling feel and hides the scrollbar:
.scroll-momentum {
-webkit-overflow-scrolling: touch;
overflow-x: auto;
overscroll-behavior-x: contain;
scrollbar-width: none;
}
.scroll-momentum::-webkit-scrollbar {
display: none;
}And because mobile users can't hover, every cell now responds to touch:
onTouchStart={e => {
const rect = e.currentTarget.getBoundingClientRect();
setTooltip({
index: yearIdx,
x: rect.left + rect.width / 2,
y: rect.top,
});
}}
onTouchEnd={() => {
setTimeout(() => setTooltip(null), 2000);
}}Tap a cell, the tooltip appears for two seconds, then fades. Same information as the desktop hover, adapted for fingers.
✅ Fix 3: The Tooltip Clamping Problem
The desktop tooltip uses position: fixed and centers itself above the hovered cell. This works perfectly on a 1440px screen. On a 375px phone, a tooltip positioned at left: 350px with min-width: 230px extends 80px past the right edge of the viewport.
The fix clamps the tooltip position to the visible area:
left: isMobile
? Math.min(Math.max(tooltip.x, 140), window.innerWidth - 140)
: tooltip.x,
top: isMobile
? Math.max(tooltip.y - 12, 80)
: tooltip.y - 12,The 140 is half of the tooltip's approximate width (~280px on mobile). By clamping the center point to [140, screen - 140], the tooltip never overflows. The top clamp prevents it from disappearing behind the fixed navigation bar. Small math, big UX difference.
✅ Fix 4: The SVG Chart Touch Support
The fortune timeline chart is an SVG with onMouseMove tracking. On mobile, mouse events don't fire. The chart was completely dead to touch.
Adding touch support required mirroring the mouse logic for onTouchMove and onTouchEnd:
onTouchMove={e => {
const svg = svgRef.current;
if (!svg || !e.touches[0]) return;
const rect = svg.getBoundingClientRect();
const svgX =
((e.touches[0].clientX - rect.left) / rect.width) * W;
if (svgX < PL || svgX > W - PR) {
onHover(null, 0, 0);
return;
}
const idx = Math.round(((svgX - PL) / IW) * (total - 1));
const clamped = Math.max(0, Math.min(total - 1, idx));
onHover(clamped, e.touches[0].clientX, screenY);
}}
onTouchEnd={() => setTimeout(() => onHover(null, 0, 0), 1500)}Now you can drag your finger along the chart and see each year's fortune data appear in the tooltip. The 1.5-second delay on onTouchEnd gives users time to read the tooltip before it disappears.
✅ Fix 5: Viral Sharing — Beyond Clipboard Copy
The original share implementation was a single button that copied the URL to clipboard. On desktop, that's fine. On mobile, people share through specific apps — iMessage, KakaoTalk, Instagram Stories, Twitter. A clipboard URL is a dead end.
The new approach layers three sharing mechanisms:
Web Share API (native mobile share sheet)
if (typeof navigator.share === "function") {
try {
await navigator.share({ title, text, url });
return;
} catch {
// User cancelled — fall through to clipboard
}
}On iOS Safari and Android Chrome, this triggers the native share sheet — the same one you see when sharing a photo. Users see their actual messaging apps. This is the highest-conversion sharing mechanism on mobile because it requires zero friction: one tap, pick the app, send.
Platform-Specific Share Buttons
For users whose browsers don't support Web Share API (or who prefer a specific platform), there's a row of share pills:
<button onClick={() => handleShareTo("twitter")} style={sharePillStyle}>
𝕏
</button>
<button onClick={() => handleShareTo("facebook")} style={sharePillStyle}>
f
</button>
<button onClick={() => handleShareTo("kakao")} style={sharePillStyle}>
Kakao
</button>
<button onClick={() => handleShareTo("kakao")} style={sharePillStyle}>
🔗
</button>The handleShareTo function opens the platform's share URL in a popup window. For KakaoStory (critical for the Korean market), the share text includes the zodiac emoji and sign name — "I'm Scorpio ♏ — my entire life mapped by the stars" — because personalized share text gets significantly more engagement than generic URLs.
The Screenshot-Friendly Share Card
This is the viral secret weapon. Below the grid, there's now a share-card component — a visually compact summary of your results designed to be screenshot-worthy:
<div className="share-card" style={{
padding: isMobile ? "1.25rem" : "2rem 2.5rem",
textAlign: "center",
}}>
<p>My Destiny Grid</p>
<p>{sign.symbol} {sign.name}</p>
<p>{sign.element} · {sign.rulingPlanet} · {city.name}</p>
{/* Stats: Lucky years, Shadow years, Next Jupiter */}
{/* Mini color strip showing your entire life */}
<p>vibed-lab.com/playground/destiny-grid</p>
</div>The card includes a mini color strip — a compressed version of your full grid — that creates a distinctive visual fingerprint of your fortune data. No two people's strips look the same (unless they share a birthday and birthplace). The CSS share-card class adds a rainbow gradient bar at the bottom:
.share-card::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg,
#7f1d1d, #ef4444, #f97316,
#fde047, #86efac, #22c55e, #38bdf8);
}This creates an immediately recognizable visual brand for the Destiny Grid. When someone screenshots this card and posts it to their Instagram story, the rainbow bar and the personal data make it both visually compelling and conversation-starting ("What's your zodiac? Check yours!"). The site URL at the bottom closes the loop.
✅ Fix 6: Global Mobile Foundations
Beyond the playground-specific changes, I added foundational mobile support across the entire site.
Viewport configuration — Next.js has a dedicated viewport export, separate from metadata:
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 5,
viewportFit: "cover",
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#FAF8F3" },
{ media: "(prefers-color-scheme: dark)", color: "#141210" },
],
};The viewportFit: "cover" ensures the content extends into the safe area on iPhones with notches. The themeColor array makes the browser chrome (address bar, status bar) match the site theme.
Safe area insets — for notched devices, the body now respects the hardware cutouts:
@supports (padding: env(safe-area-inset-top)) {
body {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
}Touch target sizing — a global rule ensures that every interactive element meets minimum touch standards on touch devices:
@media (pointer: coarse) {
button, a, select, input[type="range"] {
min-height: 44px;
}
}The pointer: coarse media query targets touch devices specifically, so desktop users don't get bloated buttons. This is more precise than using a width breakpoint, because tablets in landscape mode still need large touch targets.
✅ Fix 7: OG Tags and Twitter Cards — Making Links Look Good
When someone shares a Destiny Grid link on Twitter or in a KakaoTalk chat, the preview card is the first thing the recipient sees. The original implementation had OpenGraph tags for the Destiny Grid page, but was missing images, twitter card configuration, and the playground hub had no OG tags at all.
Now both pages have complete metadata:
// /playground/destiny-grid/page.tsx
openGraph: {
title: "Destiny Grid — Your Fortune Through the Years",
description: "Your entire life, colored by the stars...",
type: "website",
url: "https://vibed-lab.com/playground/destiny-grid",
images: [{
url: "/images/og-default.png",
width: 1200, height: 630,
alt: "Destiny Grid — See your entire life mapped by astrology",
}],
},
twitter: {
card: "summary_large_image",
title: "Destiny Grid — Your Fortune Through the Years",
description: "Your entire life, colored by the stars...",
images: ["/images/og-default.png"],
},I also expanded the Korean keyword coverage — 나의 운세, 별자리 인생 지도, 목성 리턴 — because a significant portion of the target audience searches in Korean. These keywords don't cost anything to add but help with discoverability in Korean search engines and social platforms.
🧠 The Meta-Lesson: Mobile Isn't a Viewport Size, It's a Different Product
The single biggest lesson from this work: mobile optimization isn't about making things smaller. It's about redesigning the interaction model.
On desktop, users hover. On mobile, they tap. On desktop, sharing means "copy URL." On mobile, sharing means "open the app I want to send this to." On desktop, 46px grid cells are elegant. On mobile, 30px cells with momentum scrolling and a "swipe to explore" hint are functional. On desktop, a tooltip follows the cursor. On mobile, it needs to be clamped to the viewport and auto-dismiss after a delay.
Every single change I made was about respecting the fact that a phone is a different device with different physics. Fingers are bigger than cursors. Screens are vertical, not horizontal. Sharing is social, not textual. The data is the same. The way a human interacts with it is fundamentally different.
The Destiny Grid now works on a phone. More importantly, it's designed for a phone — the stacked inputs feel intentional, the grid swipe feels natural, the share card feels like something you'd actually want to screenshot and post. That's the difference between responsive and mobile-first.
Try it yourself at vibed-lab.com/playground/destiny-grid.
Written by
Jay
Licensed Pharmacist · Senior Researcher
Building production-grade AI tools across medicine, finance, and productivity — without a CS degree. Domain expertise first, code second.
About the author →Related posts