agentsclimarketplace

Design mobile first

Skill kensaurus/cursor-kenji/skills/design-mobile-first

Designs mobile-first responsive interfaces with touch optimization — breakpoint strategy, touch targets, safe areas, and gesture handling, enhanced progressively for larger screens. Use when the user mentions "mobile", "responsive", "touch", "PWA", "small screen", "tablet", "swipe", or "gesture", or when a UI looks desktop-only on a phone viewport.From its SKILL.md

Install
npx -y skills add kensaurus/cursor-kenji --skill design-mobile-first

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

10.9 KB, ~2.9k tokens by cl100k_base, as published. Nobody here has run it

Mobile-First Design Skill

Build interfaces that work beautifully on mobile devices first, then enhance for larger screens.

CRITICAL: Check Existing First

Before ANY mobile optimization, verify:

  1. Check existing breakpoints:
cat tailwind.config.* | grep -A10 "screens\|breakpoints"
rg "sm:|md:|lg:|xl:" --type tsx | head -20
  1. Check for existing mobile patterns:
rg "useMediaQuery|useBreakpoint|isMobile" --type ts --type tsx
ls -la src/hooks/use*Mobile* src/hooks/use*Responsive* 2>/dev/null
  1. Check viewport meta:
rg "viewport" src/app/layout.tsx index.html

Why: Maintain consistent responsive patterns across the app.

Mobile-First Principles

1. Design for Mobile First

/* Base styles = mobile */
.card {
 padding: 1rem;
 font-size: 0.875rem;
}

/* Then enhance for larger screens */
@media (min-width: 640px) {
 .card {
 padding: 1.5rem;
 font-size: 1rem;
 }
}

/* Tailwind design-mobile-first */
<div className="p-4 sm:p-6 md:p-8">
<div className="text-sm sm:text-base lg:text-lg">
<div className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">

2. Touch Targets (44px minimum)

// Touch-friendly button
<button className="min-h-[44px] min-w-[44px] px-4 py-3">
 Tap me
</button>

// Touch-friendly list items
<li className="py-3 px-4 min-h-[44px] active:bg-muted">
 List item
</li>

// Icon buttons with proper touch area
<button
 className="p-3 -m-3" // Negative margin extends touch area
 aria-label="Menu"
>
 <MenuIcon className="h-5 w-5" />
</button>

3. Thumb-Friendly Zones

// Bottom navigation for primary actions
<nav className="fixed bottom-0 left-0 right-0 border-t bg-background safe-area-pb">
 <div className="flex justify-around py-2">
 <NavItem icon={HomeIcon} label="Home" />
 <NavItem icon={SearchIcon} label="Search" />
 <NavItem icon={PlusIcon} label="Create" />
 <NavItem icon={BellIcon} label="Alerts" />
 <NavItem icon={UserIcon} label="Profile" />
 </div>
</nav>

// Safe area padding for notched devices
.safe-area-pb {
 padding-bottom: env(safe-area-inset-bottom);
}

Responsive Patterns

Mobile Navigation

'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'

export function MobileNav() {
 const [isOpen, setIsOpen] = useState(false)

 return (
 <>
 <button
 className="lg:hidden p-2"
 onClick={() => setIsOpen(true)}
 aria-label="Open menu"
 >
 <MenuIcon className="h-6 w-6" />
 </button>

 <AnimatePresence>
 {isOpen && (
 <>
 {/* Backdrop */}
 <motion.div
 initial={{ opacity: 0 }}
 animate={{ opacity: 1 }}
 exit={{ opacity: 0 }}
 className="fixed inset-0 bg-black/50 z-40 lg:hidden"
 onClick={() => setIsOpen(false)}
 />

 {/* Drawer */}
 <motion.div
 initial={{ x: '-100%' }}
 animate={{ x: 0 }}
 exit={{ x: '-100%' }}
 transition={{ type: 'spring', damping: 25, stiffness: 200 }}
 className="fixed inset-y-0 left-0 w-[280px] bg-background z-50 lg:hidden"
 >
 <div className="p-4">
 <button
 className="absolute top-4 right-4 p-2"
 onClick={() => setIsOpen(false)}
 >
 <XIcon className="h-6 w-6" />
 </button>
 <nav className="mt-8 space-y-2">
 {navItems.map((item) => (
 <a
 key={item.href}
 href={item.href}
 className="block py-3 px-4 rounded-lg hover:bg-muted"
 >
 {item.label}
 </a>
 ))}
 </nav>
 </div>
 </motion.div>
 </>
 )}
 </AnimatePresence>
 </>
 )
}

Responsive Tables

// Card-based layout on mobile, table on desktop
export function ResponsiveTable({ data }: { data: Item[] }) {
 return (
 <>
 {/* Mobile: Card layout */}
 <div className="space-y-4 md:hidden">
 {data.map((item) => (
 <div key={item.id} className="rounded-lg border p-4">
 <div className="flex justify-between">
 <span className="font-medium">{item.name}</span>
 <span className="text-muted-foreground">{item.date}</span>
 </div>
 <div className="mt-2 text-sm text-muted-foreground">
 {item.description}
 </div>
 <div className="mt-3 flex justify-between items-center">
 <span className="font-semibold">{item.amount}</span>
 <Badge>{item.status}</Badge>
 </div>
 </div>
 ))}
 </div>

 {/* Desktop: Traditional table */}
 <table className="hidden md:table w-full">
 <thead>
 <tr className="border-b">
 <th className="text-left py-3">Name</th>
 <th className="text-left py-3">Date</th>
 <th className="text-left py-3">Amount</th>
 <th className="text-left py-3">Status</th>
 </tr>
 </thead>
 <tbody>
 {data.map((item) => (
 <tr key={item.id} className="border-b">
 <td className="py-3">{item.name}</td>
 <td className="py-3">{item.date}</td>
 <td className="py-3">{item.amount}</td>
 <td className="py-3"><Badge>{item.status}</Badge></td>
 </tr>
 ))}
 </tbody>
 </table>
 </>
 )
}

Responsive Grid

// Fluid grid that adapts to screen size
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
 {items.map((item) => (
 <Card key={item.id}>{item.content}</Card>
 ))}
</div>

// Auto-fit grid (items wrap naturally)
<div className="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-4">
 {items.map((item) => (
 <Card key={item.id}>{item.content}</Card>
 ))}
</div>

Touch Gestures

Swipe to Delete

'use client'
import { motion, useMotionValue, useTransform, PanInfo } from 'framer-motion'

export function SwipeableItem({ onDelete, children }: SwipeableItemProps) {
 const x = useMotionValue(0)
 const background = useTransform(
 x,
 [-100, 0],
 ['rgb(239 68 68)', 'rgb(255 255 255)']
 )

 const handleDragEnd = (_: any, info: PanInfo) => {
 if (info.offset.x < -100) {
 onDelete()
 }
 }

 return (
 <motion.div style={{ background }} className="relative overflow-hidden rounded-lg">
 <motion.div
 drag="x"
 dragConstraints={{ left: -100, right: 0 }}
 onDragEnd={handleDragEnd}
 style={{ x }}
 className="bg-background p-4"
 >
 {children}
 </motion.div>
 <div className="absolute right-4 top-1/2 -translate-y-1/2 text-white">
 <TrashIcon className="h-5 w-5" />
 </div>
 </motion.div>
 )
}

Pull to Refresh

'use client'
import { useState, useRef } from 'react'
import { motion, useMotionValue, useTransform } from 'framer-motion'

export function PullToRefresh({ onRefresh, children }: PullToRefreshProps) {
 const [isRefreshing, setIsRefreshing] = useState(false)
 const y = useMotionValue(0)
 const opacity = useTransform(y, [0, 60], [0, 1])
 const scale = useTransform(y, [0, 60], [0.5, 1])

 const handleDragEnd = async () => {
 if (y.get() > 60) {
 setIsRefreshing(true)
 await onRefresh()
 setIsRefreshing(false)
 }
 }

 return (
 <div className="overflow-hidden">
 <motion.div
 style={{ opacity, scale }}
 className="flex justify-center py-4"
 >
 <RefreshIcon className={cn('h-6 w-6', isRefreshing && 'animate-spin')} />
 </motion.div>
 <motion.div
 drag="y"
 dragConstraints={{ top: 0, bottom: 0 }}
 dragElastic={0.5}
 onDragEnd={handleDragEnd}
 style={{ y }}
 >
 {children}
 </motion.div>
 </div>
 )
}

Mobile-Specific Components

Bottom Sheet

'use client'
import { motion, useDragControls, PanInfo } from 'framer-motion'

export function BottomSheet({ isOpen, onClose, children }: BottomSheetProps) {
 const controls = useDragControls()

 const handleDragEnd = (_: any, info: PanInfo) => {
 if (info.velocity.y > 500 || info.offset.y > 200) {
 onClose()
 }
 }

 if (!isOpen) return null

 return (
 <>
 <motion.div
 initial={{ opacity: 0 }}
 animate={{ opacity: 1 }}
 exit={{ opacity: 0 }}
 className="fixed inset-0 bg-black/50 z-40"
 onClick={onClose}
 />
 <motion.div
 initial={{ y: '100%' }}
 animate={{ y: 0 }}
 exit={{ y: '100%' }}
 drag="y"
 dragControls={controls}
 dragConstraints={{ top: 0 }}
 dragElastic={0.2}
 onDragEnd={handleDragEnd}
 className="fixed bottom-0 left-0 right-0 bg-background rounded-t-xl z-50 max-h-[90vh] overflow-hidden"
 >
 {/* Drag handle */}
 <div
 className="flex justify-center py-3 cursor-grab active:cursor-grabbing"
 onPointerDown={(e) => controls.start(e)}
 >
 <div className="w-12 h-1.5 bg-muted-foreground/30 rounded-full" />
 </div>
 <div className="px-4 pb-safe overflow-y-auto max-h-[calc(90vh-40px)]">
 {children}
 </div>
 </motion.div>
 </>
 )
}

Mobile-Optimized Forms

// Stack form fields vertically on mobile
<form className="space-y-4">
 <div className="grid gap-4 sm:grid-cols-2">
 <div className="space-y-2">
 <Label htmlFor="firstName">First Name</Label>
 <Input
 id="firstName"
 // Larger touch targets
 className="h-12 text-base"
 // Prevent iOS zoom on focus
 style={{ fontSize: '16px' }}
 />
 </div>
 <div className="space-y-2">
 <Label htmlFor="lastName">Last Name</Label>
 <Input
 id="lastName"
 className="h-12 text-base"
 style={{ fontSize: '16px' }}
 />
 </div>
 </div>

 {/* Full-width button on mobile */}
 <Button className="w-full sm:w-auto h-12">
 Submit
 </Button>
</form>

PWA Features

Viewport & Meta Tags

// app/layout.tsx
export const metadata = {
 viewport: {
 width: 'device-width',
 initialScale: 1,
 maximumScale: 1, // Prevents zoom on input focus
 userScalable: false,
 viewportFit: 'cover', // For notched devices
 },
 appleWebApp: {
 capable: true,
 statusBarStyle: 'default',
 title: 'My App',
 },
}

Install Prompt

'use client'
import { useEffect, useState } from 'react'

export function InstallPrompt() {
 const [deferredPrompt, setDeferredPrompt] = useState<any>(null)

 useEffect(() => {
 const handler = (e: Event) => {
 e.preventDefault()
 setDeferredPrompt(e)
 }

 window.addEventListener('beforeinstallprompt', handler)
 return () => window.removeEventListener('beforeinstallprompt', handler)
 }, [])

 if (!deferredPrompt) return null

 const handleInstall = async () => {
 deferredPrompt.prompt()
 const { outcome } = await deferredPrompt.userChoice
 if (outcome === 'accepted') {
 setDeferredPrompt(null)
 }
 }

 return (
 <div className="fixed bottom-20 left-4 right-4 p-4 bg-card border rounded-lg shadow-lg">
 <p className="font-medium">Install our app</p>
 <p className="text-sm text-muted-foreground">Get quick access from your home screen</p>
 <div className="mt-3 flex gap-2">
 <Button onClick={handleInstall}>Install</Button>
 <Button variant="ghost" onClick={() => setDeferredPrompt(null)}>
 Not now
 </Button>
 </div>
 </div>
 )
}

Validation

After implementing mobile features:

  1. Touch targets → All interactive elements ≥44px
  2. Thumb zones → Primary actions reachable with thumb
  3. Gestures → Natural, discoverable, with visual feedback
  4. Performance → 60fps scrolling, no jank
  5. Forms → No zoom on focus, appropriate keyboards
  6. Safe areas → Content respects notches and home indicators
  7. Offline → Graceful handling when offline
  8. Real device test → Test on actual phones, not just emulators

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,782. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.