Back to skills
extension
Category: Development & EngineeringNo API key required

responsive-design

Implement mobile-first responsive design with Tailwind CSS breakpoints for mobile, tablet, and desktop. Use when building responsive layouts or fixing layout issues.

personAuthor: jakexiaohubgithub

You implement responsive design for the QA Team Portal using Tailwind CSS mobile-first approach.

Requirements from PROJECT_PLAN.md

  • Responsive: Mobile, tablet, desktop support
  • Mobile-first design approach
  • Touch-friendly interface
  • Breakpoints: sm (640px), md (768px), lg (1024px), xl (1280px)
  • Responsive images and media
  • Flexible layouts with CSS Grid and Flexbox

Tailwind CSS Breakpoints

sm:  @media (min-width: 640px)   - Mobile landscape, small tablets
md:  @media (min-width: 768px)   - Tablets
lg:  @media (min-width: 1024px)  - Desktop
xl:  @media (min-width: 1280px)  - Large desktop
2xl: @media (min-width: 1536px)  - Extra large desktop

Implementation

1. Mobile-First Layout Pattern

Start with mobile styles, then add larger breakpoints:

// ❌ Wrong: Desktop-first approach
<div className="grid grid-cols-4 md:grid-cols-2 sm:grid-cols-1">

// ✅ Correct: Mobile-first approach
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">

2. Responsive Navigation

Location: frontend/src/components/Navigation.tsx

import { useState } from 'react'
import { Menu, X } from 'lucide-react'
import { Button } from '@/components/ui/button'

export const Navigation = () => {
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false)

  return (
    <header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur">
      <div className="container mx-auto px-4">
        <div className="flex h-16 items-center justify-between">
          {/* Logo */}
          <div className="flex items-center gap-2">
            <img src="/logo.svg" alt="Evoke" className="h-8 w-8" />
            <span className="hidden sm:inline font-heading text-xl font-bold">
              QA Team Portal
            </span>
          </div>

          {/* Desktop Navigation */}
          <nav className="hidden md:flex items-center gap-6">
            <a href="#team" className="text-sm font-medium hover:text-primary transition-colors">
              Team
            </a>
            <a href="#updates" className="text-sm font-medium hover:text-primary transition-colors">
              Updates
            </a>
            <a href="#tools" className="text-sm font-medium hover:text-primary transition-colors">
              Tools
            </a>
            <a href="#resources" className="text-sm font-medium hover:text-primary transition-colors">
              Resources
            </a>
            <Button size="sm">Admin Portal</Button>
          </nav>

          {/* Mobile Menu Button */}
          <Button
            variant="ghost"
            size="icon"
            className="md:hidden"
            onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
            aria-label="Toggle menu"
          >
            {mobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
          </Button>
        </div>

        {/* Mobile Navigation */}
        {mobileMenuOpen && (
          <nav className="md:hidden py-4 border-t border-border animate-slide-in">
            <div className="flex flex-col gap-4">
              <a
                href="#team"
                className="text-base font-medium hover:text-primary transition-colors"
                onClick={() => setMobileMenuOpen(false)}
              >
                Team
              </a>
              <a
                href="#updates"
                className="text-base font-medium hover:text-primary transition-colors"
                onClick={() => setMobileMenuOpen(false)}
              >
                Updates
              </a>
              <a
                href="#tools"
                className="text-base font-medium hover:text-primary transition-colors"
                onClick={() => setMobileMenuOpen(false)}
              >
                Tools
              </a>
              <a
                href="#resources"
                className="text-base font-medium hover:text-primary transition-colors"
                onClick={() => setMobileMenuOpen(false)}
              >
                Resources
              </a>
              <Button className="w-full">Admin Portal</Button>
            </div>
          </nav>
        )}
      </div>
    </header>
  )
}

3. Responsive Grid Layouts

Team Members Grid:

// frontend/src/components/TeamGrid.tsx
export const TeamGrid = ({ members }: { members: TeamMember[] }) => {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6">
      {members.map((member) => (
        <TeamCard key={member.id} member={member} />
      ))}
    </div>
  )
}

Tools by Category:

// frontend/src/components/ToolsSection.tsx
export const ToolsSection = ({ tools }: { tools: Tool[] }) => {
  return (
    <div className="space-y-8">
      {Object.entries(toolsByCategory).map(([category, categoryTools]) => (
        <div key={category}>
          <h3 className="text-xl sm:text-2xl font-bold mb-4">{category}</h3>
          <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
            {categoryTools.map((tool) => (
              <ToolCard key={tool.id} tool={tool} />
            ))}
          </div>
        </div>
      ))}
    </div>
  )
}

4. Responsive Typography

// frontend/src/components/Hero.tsx
export const Hero = () => {
  return (
    <section className="py-12 sm:py-16 md:py-20 lg:py-32">
      <div className="container mx-auto px-4">
        <div className="max-w-3xl mx-auto text-center">
          {/* Responsive heading sizes */}
          <h1 className="font-heading text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-balance mb-4 sm:mb-6">
            Welcome to Evoke QA Team
          </h1>

          {/* Responsive paragraph sizes */}
          <p className="text-base sm:text-lg md:text-xl text-muted-foreground text-balance mb-6 sm:mb-8">
            Building quality software through comprehensive testing and collaboration
          </p>

          {/* Responsive button sizes */}
          <Button size="default" className="w-full sm:w-auto">
            Get Started
          </Button>
        </div>
      </div>
    </section>
  )
}

5. Responsive Spacing

// Use responsive padding, margin, gap
<div className="
  p-4 sm:p-6 lg:p-8           // Padding increases on larger screens
  mb-4 sm:mb-6 lg:mb-8        // Margin increases
  space-y-4 sm:space-y-6      // Gap between children increases
">
  <h2 className="text-2xl sm:text-3xl lg:text-4xl">Title</h2>
  <p className="text-sm sm:text-base lg:text-lg">Content</p>
</div>

6. Responsive Card Component

// frontend/src/components/TeamCard.tsx
export const TeamCard = ({ member }: { member: TeamMember }) => {
  return (
    <Card className="group h-full flex flex-col">
      {/* Responsive image aspect ratio */}
      <div className="relative overflow-hidden rounded-t-lg aspect-square sm:aspect-[4/3] lg:aspect-square">
        <img
          src={member.photo_url}
          alt={member.name}
          className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
        />
      </div>

      <CardContent className="flex-1 p-4 sm:p-6">
        {/* Responsive text sizes */}
        <h3 className="font-heading text-lg sm:text-xl font-semibold mb-1">
          {member.name}
        </h3>
        <p className="text-sm sm:text-base text-primary-600 mb-2 sm:mb-3">
          {member.role}
        </p>

        {/* Hide bio on very small screens */}
        {member.bio && (
          <p className="hidden sm:block text-sm text-muted-foreground line-clamp-3">
            {member.bio}
          </p>
        )}
      </CardContent>
    </Card>
  )
}

7. Responsive Table

Desktop: Full table, Mobile: Card layout:

// frontend/src/components/admin/TeamMembersTable.tsx
export const TeamMembersTable = ({ members }: { members: TeamMember[] }) => {
  return (
    <>
      {/* Desktop Table - hidden on mobile */}
      <div className="hidden md:block overflow-x-auto">
        <table className="w-full">
          <thead>
            <tr className="border-b">
              <th className="text-left p-4">Photo</th>
              <th className="text-left p-4">Name</th>
              <th className="text-left p-4">Role</th>
              <th className="text-left p-4">Email</th>
              <th className="text-right p-4">Actions</th>
            </tr>
          </thead>
          <tbody>
            {members.map((member) => (
              <tr key={member.id} className="border-b">
                <td className="p-4">
                  <img src={member.photo_url} className="w-12 h-12 rounded-full" />
                </td>
                <td className="p-4">{member.name}</td>
                <td className="p-4">{member.role}</td>
                <td className="p-4">{member.email}</td>
                <td className="p-4 text-right">
                  <Button variant="ghost" size="sm">Edit</Button>
                  <Button variant="ghost" size="sm">Delete</Button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Mobile Card Layout - hidden on desktop */}
      <div className="md:hidden space-y-4">
        {members.map((member) => (
          <Card key={member.id}>
            <CardContent className="p-4">
              <div className="flex items-start gap-4">
                <img src={member.photo_url} className="w-16 h-16 rounded-full" />
                <div className="flex-1 min-w-0">
                  <h3 className="font-semibold truncate">{member.name}</h3>
                  <p className="text-sm text-muted-foreground">{member.role}</p>
                  <p className="text-sm text-muted-foreground truncate">{member.email}</p>
                </div>
              </div>
              <div className="flex gap-2 mt-4">
                <Button variant="outline" size="sm" className="flex-1">Edit</Button>
                <Button variant="outline" size="sm" className="flex-1">Delete</Button>
              </div>
            </CardContent>
          </Card>
        ))}
      </div>
    </>
  )
}

8. Responsive Form Layout

// frontend/src/components/admin/TeamMemberForm.tsx
export const TeamMemberForm = () => {
  return (
    <form className="space-y-6">
      {/* Single column on mobile, two columns on desktop */}
      <div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
        <div className="space-y-2">
          <Label htmlFor="name">Name</Label>
          <Input id="name" />
        </div>

        <div className="space-y-2">
          <Label htmlFor="role">Role</Label>
          <Input id="role" />
        </div>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
        <div className="space-y-2">
          <Label htmlFor="email">Email</Label>
          <Input id="email" type="email" />
        </div>

        <div className="space-y-2">
          <Label htmlFor="photo">Photo</Label>
          <Input id="photo" type="file" />
        </div>
      </div>

      {/* Full width */}
      <div className="space-y-2">
        <Label htmlFor="bio">Bio</Label>
        <Textarea id="bio" rows={4} />
      </div>

      {/* Responsive button layout */}
      <div className="flex flex-col sm:flex-row gap-3 sm:justify-end">
        <Button type="button" variant="outline" className="w-full sm:w-auto">
          Cancel
        </Button>
        <Button type="submit" className="w-full sm:w-auto">
          Save Changes
        </Button>
      </div>
    </form>
  )
}

9. Responsive Images

// frontend/src/components/OptimizedImage.tsx
export const ResponsiveImage = ({ src, alt }: { src: string; alt: string }) => {
  return (
    <picture>
      {/* Mobile: 640w */}
      <source
        media="(max-width: 640px)"
        srcSet={`${src}?w=640 1x, ${src}?w=1280 2x`}
      />
      {/* Tablet: 1024w */}
      <source
        media="(max-width: 1024px)"
        srcSet={`${src}?w=1024 1x, ${src}?w=2048 2x`}
      />
      {/* Desktop: 1920w */}
      <source
        media="(min-width: 1025px)"
        srcSet={`${src}?w=1920 1x, ${src}?w=3840 2x`}
      />
      {/* Fallback */}
      <img
        src={src}
        alt={alt}
        loading="lazy"
        decoding="async"
        className="w-full h-auto"
      />
    </picture>
  )
}

10. Responsive Container

// frontend/src/components/Container.tsx
export const Container = ({ children, className }: { children: React.ReactNode; className?: string }) => {
  return (
    <div className={cn(
      "mx-auto w-full",
      "px-4 sm:px-6 lg:px-8",          // Responsive horizontal padding
      "max-w-7xl",                      // Maximum width
      className
    )}>
      {children}
    </div>
  )
}

11. Touch-Friendly Targets

// Ensure minimum 44x44px touch targets on mobile
<Button
  size="default"  // default is 40px height
  className="h-11 px-4 sm:h-10"  // 44px on mobile, 40px on desktop
>
  Click Me
</Button>

// Increase spacing between interactive elements
<div className="flex gap-3 sm:gap-2">
  <Button>Action 1</Button>
  <Button>Action 2</Button>
  <Button>Action 3</Button>
</div>

12. Responsive Sidebar Layout

// frontend/src/layouts/AdminLayout.tsx
export const AdminLayout = ({ children }: { children: React.ReactNode }) => {
  const [sidebarOpen, setSidebarOpen] = useState(false)

  return (
    <div className="min-h-screen bg-background">
      {/* Mobile Sidebar Overlay */}
      {sidebarOpen && (
        <div
          className="fixed inset-0 bg-black/50 z-40 lg:hidden"
          onClick={() => setSidebarOpen(false)}
        />
      )}

      {/* Sidebar */}
      <aside className={cn(
        "fixed inset-y-0 left-0 z-50 w-64 bg-card border-r transform transition-transform",
        "lg:translate-x-0 lg:static",  // Always visible on desktop
        sidebarOpen ? "translate-x-0" : "-translate-x-full"  // Slide in/out on mobile
      )}>
        <nav className="p-4 space-y-2">
          <a href="/admin/dashboard" className="block px-4 py-2 rounded-lg hover:bg-accent">
            Dashboard
          </a>
          <a href="/admin/team-members" className="block px-4 py-2 rounded-lg hover:bg-accent">
            Team Members
          </a>
          <a href="/admin/tools" className="block px-4 py-2 rounded-lg hover:bg-accent">
            Tools
          </a>
        </nav>
      </aside>

      {/* Main Content */}
      <div className="lg:pl-64">
        {/* Header with mobile menu button */}
        <header className="sticky top-0 z-30 border-b bg-background px-4 sm:px-6 h-16 flex items-center gap-4">
          <Button
            variant="ghost"
            size="icon"
            className="lg:hidden"
            onClick={() => setSidebarOpen(true)}
          >
            <Menu className="h-6 w-6" />
          </Button>
          <h1 className="text-xl font-bold">Admin Portal</h1>
        </header>

        {/* Page Content */}
        <main className="p-4 sm:p-6 lg:p-8">
          {children}
        </main>
      </div>
    </div>
  )
}

Responsive Design Checklist

Layout

  • [ ] Mobile-first approach used (base styles for mobile, breakpoints for larger)
  • [ ] Grid layouts adjust columns based on screen size
  • [ ] Sidebar collapses on mobile with hamburger menu
  • [ ] Containers use responsive padding (px-4 sm:px-6 lg:px-8)
  • [ ] Max-width set for very large screens
  • [ ] Flexbox direction changes on smaller screens (flex-col sm:flex-row)

Typography

  • [ ] Headings use responsive sizes (text-3xl sm:text-4xl lg:text-5xl)
  • [ ] Body text readable on all devices (text-base sm:text-lg)
  • [ ] Line height appropriate for small screens
  • [ ] Text balance used for headings

Navigation

  • [ ] Desktop: Horizontal navigation bar
  • [ ] Mobile: Hamburger menu with slide-out drawer
  • [ ] Touch targets >= 44x44px on mobile
  • [ ] Active state visible on all devices

Images

  • [ ] Responsive images with srcset
  • [ ] Aspect ratios adjust for different screens
  • [ ] Lazy loading enabled
  • [ ] WebP format with fallback

Forms

  • [ ] Single column on mobile, multiple columns on desktop
  • [ ] Labels above inputs on mobile
  • [ ] Full-width buttons on mobile, auto-width on desktop
  • [ ] Adequate spacing between form fields

Tables

  • [ ] Desktop: Full table
  • [ ] Mobile: Card layout or horizontal scroll
  • [ ] Important columns visible on small screens
  • [ ] Sticky header on scroll (optional)

Testing

  • [ ] Tested on real devices (iOS, Android)
  • [ ] Tested on different browsers (Chrome, Safari, Firefox)
  • [ ] Tested all breakpoints (sm, md, lg, xl)
  • [ ] Tested landscape and portrait orientations
  • [ ] Touch interactions work smoothly
  • [ ] No horizontal scrolling on any screen size

Testing Responsive Design

Chrome DevTools:

1. Open DevTools (F12)
2. Click "Toggle device toolbar" (Ctrl+Shift+M)
3. Test different devices and screen sizes
4. Check both portrait and landscape

Test Breakpoints:

// Add to development environment
const breakpoints = [
  { name: 'Mobile', width: 375, height: 667 },
  { name: 'Mobile L', width: 425, height: 667 },
  { name: 'Tablet', width: 768, height: 1024 },
  { name: 'Laptop', width: 1024, height: 768 },
  { name: 'Desktop', width: 1440, height: 900 },
]

Playwright Responsive Tests:

# tests/e2e/test_responsive.py
def test_mobile_layout(page):
    page.set_viewport_size({'width': 375, 'height': 667})
    page.goto('http://localhost:5173')

    # Mobile menu should be visible
    assert page.locator('[data-testid="mobile-menu-button"]').is_visible()

    # Desktop menu should be hidden
    assert not page.locator('[data-testid="desktop-menu"]').is_visible()

def test_desktop_layout(page):
    page.set_viewport_size({'width': 1920, 'height': 1080})
    page.goto('http://localhost:5173')

    # Desktop menu should be visible
    assert page.locator('[data-testid="desktop-menu"]').is_visible()

    # Mobile menu button should be hidden
    assert not page.locator('[data-testid="mobile-menu-button"]').is_visible()

Common Responsive Patterns

// Hide/Show based on screen size
<div className="hidden md:block">Desktop only</div>
<div className="block md:hidden">Mobile only</div>

// Change layout direction
<div className="flex flex-col md:flex-row">...</div>

// Adjust gap/spacing
<div className="space-y-4 md:space-y-6 lg:space-y-8">...</div>
<div className="gap-3 md:gap-4 lg:gap-6">...</div>

// Full width on mobile, auto on desktop
<Button className="w-full md:w-auto">Click Me</Button>

// Responsive grid columns
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">...</div>

// Responsive font sizes
<h1 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl">Title</h1>

// Responsive padding
<div className="p-4 sm:p-6 md:p-8 lg:p-12">Content</div>

Report

✅ Mobile-first approach implemented ✅ All breakpoints defined (sm, md, lg, xl) ✅ Responsive navigation with mobile menu ✅ Grid layouts adjust for different screens ✅ Typography scales responsively ✅ Forms optimized for mobile input ✅ Tables convert to cards on mobile ✅ Touch targets >= 44x44px ✅ Images responsive with srcset ✅ Tested on multiple devices and browsers