现代 Web 应用需要在从手机到大屏显示器的各种设备上正常工作。TailwindCSS 采用移动优先(Mobile-First)的响应式策略,通过断点前缀让你可以精确控制不同屏幕尺寸下的样式。v4 还引入了容器查询支持,让响应式设计更加灵活。
移动优先策略#
核心概念#
移动优先意味着:
- 默认样式针对最小屏幕
- 使用断点逐步增强大屏样式
<!-- 移动优先的写法 --><div class="text-sm md:text-base lg:text-lg">文字大小随屏幕增大</div>这段代码的含义:
- 默认(手机):
text-sm(14px) md及以上(平板):text-base(16px)lg及以上(桌面):text-lg(18px)
为什么是移动优先#
移动设备的限制更多(屏幕小、带宽有限),从约束最多的场景开始设计:
// 移动优先:从简单到复杂function ProductGrid({ products }: { products: Product[] }) { return ( <div className=" grid grid-cols-1 /* 手机:1列 */ sm:grid-cols-2 /* 小平板:2列 */ lg:grid-cols-3 /* 桌面:3列 */ xl:grid-cols-4 /* 大屏:4列 */ gap-4 sm:gap-6 lg:gap-8 " > {products.map((product) => ( <ProductCard key={product.id} product={product} /> ))} </div> )}断点系统#
默认断点#
v4 提供 5 个默认断点,采用 min-width 媒体查询:
| 断点 | 最小宽度 | 典型设备 |
|---|---|---|
sm | 640px | 大手机/小平板 |
md | 768px | 平板 |
lg | 1024px | 笔记本 |
xl | 1280px | 桌面显示器 |
2xl | 1536px | 大屏显示器 |
记忆技巧:sm/md/lg/xl 分别对应 640/768/1024/1280,每级约增加 256px
使用断点#
<!-- 显示/隐藏 --><div class="hidden md:block">平板及以上显示</div><div class="md:hidden">仅手机显示</div>
<!-- 布局切换 --><div class="flex flex-col md:flex-row">手机垂直排列,平板水平排列</div>
<!-- 间距调整 --><section class="py-8 md:py-12 lg:py-16">响应式内边距</section>
<!-- 网格列数 --><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4">响应式网格</div>范围断点#
v4 支持最大宽度断点:
<!-- max-* 最大宽度断点 --><div class="max-md:text-center">仅在 md 断点以下居中</div>
<!-- 范围断点 --><div class="md:max-lg:bg-blue-500">仅在 md 到 lg 之间显示蓝色背景</div>自定义断点#
在 CSS 中添加自定义断点:
@import 'tailwindcss';
@theme { --breakpoint-xs: 475px; --breakpoint-3xl: 1920px;}使用自定义断点:
<div class="xs:text-sm 3xl:text-xl">使用自定义断点</div>响应式设计模式#
响应式字体#
<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold"> 响应式标题</h1>
<p class="text-sm sm:text-base lg:text-lg leading-relaxed">响应式正文</p>实用的字体比例:
const headingStyles = { h1: 'text-3xl sm:text-4xl lg:text-5xl xl:text-6xl', h2: 'text-2xl sm:text-3xl lg:text-4xl', h3: 'text-xl sm:text-2xl lg:text-3xl', h4: 'text-lg sm:text-xl lg:text-2xl',}响应式间距#
<!-- 容器内边距 --><div class="px-4 sm:px-6 lg:px-8">内容区域</div>
<!-- 区块间距 --><section class="py-12 sm:py-16 lg:py-20 xl:py-24">大区块</section>
<!-- 元素间隙 --><div class="space-y-4 sm:space-y-6 lg:space-y-8">堆叠元素</div>响应式布局#
从堆叠到并排:
function Feature({ icon, title, description }: FeatureProps) { return ( <div className=" flex flex-col /* 手机:垂直堆叠 */ sm:flex-row /* 平板:水平排列 */ items-start sm:items-center gap-4 " > <div className="shrink-0 size-12 rounded-lg bg-blue-100 flex items-center justify-center"> {icon} </div> <div> <h3 className="font-bold text-lg">{title}</h3> <p className="text-gray-600 mt-1">{description}</p> </div> </div> )}响应式网格#
function Dashboard() { return ( <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 lg:gap-6"> {/* 指标卡片 */} <StatsCard title="总用户" value="12,345" /> <StatsCard title="活跃用户" value="8,901" /> <StatsCard title="收入" value="¥234,567" /> <StatsCard title="订单" value="1,234" />
{/* 跨列图表 */} <div className="md:col-span-2 xl:col-span-2"> <Chart type="line" /> </div> <div className="md:col-span-2 xl:col-span-2"> <Chart type="bar" /> </div>
{/* 全宽表格 */} <div className="md:col-span-2 xl:col-span-4"> <DataTable /> </div> </div> )}容器查询#
v4 引入了容器查询支持,让组件可以根据其容器(而非视口)的尺寸响应式变化。
基础用法#
<!-- 定义查询容器 --><div class="@container"> <!-- 根据容器宽度响应 --> <div class="@sm:flex @lg:grid @lg:grid-cols-2">容器查询布局</div></div>容器断点#
默认容器断点:
| 断点 | 最小宽度 |
|---|---|
@xs | 320px |
@sm | 384px |
@md | 448px |
@lg | 512px |
@xl | 576px |
@2xl | 672px |
@3xl | 768px |
@4xl | 896px |
@5xl | 1024px |
实战:响应式卡片组件#
function ArticleCard({ article }: { article: Article }) { return ( // 容器 <article className="@container bg-white rounded-xl shadow-sm overflow-hidden"> <div className=" flex flex-col /* 默认:垂直布局 */ @md:flex-row /* 容器 ≥448px:水平布局 */ " > {/* 图片 */} <div className=" aspect-video /* 默认:16:9 */ @md:aspect-square /* 水平布局时:正方形 */ @md:w-48 /* 固定宽度 */ shrink-0 " > <img src={article.image} alt={article.title} className="w-full h-full object-cover" /> </div>
{/* 内容 */} <div className="p-4 @lg:p-6"> <h3 className=" text-lg @lg:text-xl font-bold " > {article.title} </h3> <p className=" mt-2 text-sm @lg:text-base text-gray-600 line-clamp-2 @md:line-clamp-3 " > {article.excerpt} </p> </div> </div> </article> )}这个卡片组件可以放在任何宽度的容器中,并自动调整布局。
命名容器#
当需要查询特定祖先容器时:
<div class="@container/main"> <div class="@container/sidebar"> <!-- 查询 sidebar 容器 --> <div class="@md/sidebar:hidden">...</div>
<!-- 查询 main 容器 --> <div class="@lg/main:grid-cols-2">...</div> </div></div>响应式图片与媒体#
响应式图片#
<!-- 基础响应式图片 --><img src="/image.jpg" alt="描述" class="w-full max-w-md mx-auto" />
<!-- 不同尺寸的图片 --><img src="/image.jpg" alt="描述" class=" w-full md:w-1/2 lg:w-1/3 "/>
<!-- 固定宽高比 --><div class="aspect-video"> <img src="/image.jpg" alt="描述" class="w-full h-full object-cover" /></div>响应式宽高比#
<!-- 手机 1:1,平板 4:3,桌面 16:9 --><div class="aspect-square md:aspect-[4/3] lg:aspect-video"> <img src="/image.jpg" alt="描述" class="w-full h-full object-cover" /></div>响应式背景图#
<div class=" bg-cover bg-center h-[300px] md:h-[400px] lg:h-[500px] bg-[url('/hero-mobile.jpg')] md:bg-[url('/hero-tablet.jpg')] lg:bg-[url('/hero-desktop.jpg')]"> 响应式背景</div>响应式视频#
function VideoEmbed({ src }: { src: string }) { return ( <div className="aspect-video w-full max-w-4xl mx-auto"> <iframe src={src} className="w-full h-full" allowFullScreen /> </div> )}实战:响应式导航栏#
完整的响应式导航实现:
import { useState } from 'react'
function Navbar() { const [isOpen, setIsOpen] = useState(false)
const navLinks = [ { href: '/', label: '首页' }, { href: '/products', label: '产品' }, { href: '/about', label: '关于' }, { href: '/contact', label: '联系' }, ]
return ( <nav className="bg-white border-b border-gray-200"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex items-center justify-between h-16"> {/* Logo */} <div className="shrink-0"> <a href="/" className="text-xl font-bold text-gray-900"> Logo </a> </div>
{/* 桌面导航 */} <div className="hidden md:flex md:items-center md:gap-8"> {navLinks.map((link) => ( <a key={link.href} href={link.href} className="text-gray-600 hover:text-gray-900 transition-colors" > {link.label} </a> ))} <button className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"> 登录 </button> </div>
{/* 移动端菜单按钮 */} <button className="md:hidden p-2 rounded-lg hover:bg-gray-100" onClick={() => setIsOpen(!isOpen)} > <svg className="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" > {isOpen ? ( <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> ) : ( <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> )} </svg> </button> </div>
{/* 移动端菜单 */} <div className={`md:hidden ${isOpen ? 'block' : 'hidden'}`}> <div className="py-4 space-y-2"> {navLinks.map((link) => ( <a key={link.href} href={link.href} className="block py-2 px-4 text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-lg" > {link.label} </a> ))} <div className="pt-4 border-t border-gray-100"> <button className="w-full py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"> 登录 </button> </div> </div> </div> </div> </nav> )}响应式侧边栏#
function Layout({ children }: { children: React.ReactNode }) { const [sidebarOpen, setSidebarOpen] = useState(false)
return ( <div className="min-h-screen"> {/* 移动端遮罩 */} {sidebarOpen && ( <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setSidebarOpen(false)} /> )}
{/* 侧边栏 */} <aside className={` fixed top-0 left-0 z-50 w-64 h-full bg-white border-r border-gray-200 transform transition-transform duration-300 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'} lg:translate-x-0 lg:static lg:z-0 `} > <div className="p-4"> <h2 className="text-lg font-bold">侧边栏</h2> {/* 导航项 */} </div> </aside>
{/* 主内容区 */} <main className="lg:ml-64"> {/* 顶栏 */} <header className="h-16 border-b border-gray-200 flex items-center px-4 lg:px-8"> <button className="lg:hidden p-2 rounded-lg hover:bg-gray-100" onClick={() => setSidebarOpen(true)} > <svg className="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> </svg> </button> <h1 className="ml-4 lg:ml-0 text-xl font-bold">页面标题</h1> </header>
{/* 内容 */} <div className="p-4 lg:p-8">{children}</div> </main> </div> )}调试响应式#
显示当前断点#
开发时可以添加一个断点指示器:
function BreakpointIndicator() { if (process.env.NODE_ENV === 'production') return null
return ( <div className="fixed bottom-4 left-4 z-50 px-3 py-1 bg-gray-900 text-white text-sm font-mono rounded"> <span className="sm:hidden">xs</span> <span className="hidden sm:inline md:hidden">sm</span> <span className="hidden md:inline lg:hidden">md</span> <span className="hidden lg:inline xl:hidden">lg</span> <span className="hidden xl:inline 2xl:hidden">xl</span> <span className="hidden 2xl:inline">2xl</span> </div> )}浏览器开发工具#
Chrome DevTools 提供了设备模拟功能:
- 打开开发者工具(F12)
- 点击设备工具栏图标
- 选择不同设备或自定义尺寸
响应式设计让同一套代码能够适配从手机到桌面的各种设备。下一篇文章将深入探讨交互与动画,学习如何使用状态变体和过渡效果创建流畅的用户体验。