第 7 章 响应式设计
到目前为止,我们书中看到的所有示例都有一个共同点: 它们都是为电脑屏幕设计的,并没有针对更小的屏幕(如智能手机或平板)进行优化。 而让 CSS 设计能够在多种屏幕尺寸下都表现良好的过程,就叫做响应式设计(responsive design)。
在原生 CSS 中,响应式设计通常会涉及大量复杂的类名和 @media 媒体查询标签。 Tailwind 提供了一些修饰符(modifiers),可以应用到任意 Tailwind 工具类上,用来控制它们在不同屏幕尺寸下的行为。
当然,Tailwind 并不会完全消除响应式设计的复杂性; 在为多种尺寸设计时,你仍然需要考虑很多因素。 比如,需要思考哪些页面元素在小屏幕下最重要,需要重点突出。
但 Tailwind 确实让你更容易在不同屏幕尺寸下尝试不同的设计,并且可以一眼看到各个尺寸下的表现。 需要注意的是,使用 Tailwind 的响应式设计可能会导致 CSS 类名非常长,可读性会变差。
本章中,我们将了解 Tailwind 的响应式工具类,以及应用它们的一些常见模式。
Tailwind 的屏幕宽度与断点
在 CSS 中,你可以根据屏幕宽度有条件地应用各种属性,这通常通过 @media 媒体查询来实现。 设计在特定屏幕宽度发生变化的点通常称为 断点(breakpoints)。
在 Tailwind 中,你可以在任意工具类上添加 响应式修饰符(responsive modifier), 用来指定该工具类从哪个最小屏幕宽度开始生效。
Tailwind 的响应式行为和你可能习惯的其他框架略有不同,需要注意几个关键点:
- 任何响应式修饰符都会让工具类在指定屏幕宽度及更大屏幕上生效。
- Tailwind 工具类定义的是 最小宽度生效,而不是最大宽度。
- 如果没有使用修饰符,默认最小宽度是 0,也就是说该工具类始终生效。
举例来说,如果你定义了某个样式用于小屏幕,Tailwind 会让这个样式一直应用到更大屏幕——中屏、大屏甚至更大。 如果你希望某个样式只在小屏幕生效,你可以:
- 小屏幕样式不加修饰符
- 中屏或大屏时使用修饰符覆盖或取消该样式
Tailwind 默认定义了五个屏幕宽度。 对于这五种屏幕宽度,像素值指的是屏幕的 逻辑宽度(logical width)。
在视网膜屏(Retina)设备上,一个逻辑像素可能由多个物理像素组成,但 Tailwind 仍然使用逻辑宽度来计算断点。 例如,iPhone 13 的逻辑宽度是 390 像素,尽管它的物理宽度是 1170 像素。
五个默认屏幕宽度如下:
- 小屏幕(sm:)— 640 像素及以上
- 中屏幕(md:)— 768 像素及以上
- 大屏幕(lg:)— 1024 像素及以上
- 超大屏幕(xl:)— 1280 像素及以上
- 超超大屏幕(2xl:)— 1536 像素及以上
下面是部分常见设备的逻辑宽度列表:
| 设备 | 逻辑像素宽度 |
|---|---|
| Galaxy S20 | 360 |
| Galaxy S20 横屏 | 800 |
| iPhone 13 | 390 |
| iPhone 13 横屏 | 844 |
| iPad Air 3 | 834 |
| iPad Air 3 横屏 | 1112 |
| iPad Pro 12" | 1024 |
| iPad Pro 12" 横屏 | 1366 |
| MacBook Air | 2560(通常缩放为 1680) |
关键点在于,如果你定义了一个 sm: 类(例如 sm:m-2), 那么这个 m-2 会在所有宽度 640 像素及以上 的屏幕上生效。
如果你想在更大屏幕上改变这个间距,可以使用更大尺寸的修饰符。 Tailwind 保证更大尺寸的修饰符会覆盖更小尺寸的修饰符。 例如:
sm:m-2 md:m-4 lg:m-8这样,随着屏幕宽度增加,元素的外边距会逐渐变大。
一般的做法是:
- 没有修饰符的工具类描述 最小屏幕 下的样式行为
- 使用带修饰符的工具类,在屏幕变大时调整样式
也就是说,你先为移动设备设计样式,然后用修饰符为更大屏幕做调整。
书中我一直尽量保持一致,说明了默认或否定工具类(negation/default utilities)如何使用。 响应式工具类正是它们的使用场景。 Tailwind 的宽度修饰符是 从指定宽度及以上生效。
如果你想在更大屏幕上取消某个工具类,需要显式地用否定类覆盖它。 例如:
sm:shadow-xl md:shadow-none这里,.shadow-none 会在中屏(md)及以上宽度取消 .shadow-xl。 如果这样使用在一个元素上,元素在 640–768 像素宽度之间会有阴影。
值得一提的是,你可以把 屏幕尺寸修饰符和其他修饰符组合使用:
md:hover:font-bold lg:hover:font-black这是完全合法的。
基于屏幕尺寸隐藏元素
让应用在小屏幕上更适配的一种方法是:在小屏幕上隐藏部分界面元素。 在这种情况下,最小屏幕下的行为就是隐藏元素,也就是不带修饰符的默认状态为 hidden。 在更大屏幕上,你可能希望元素显示,这时就可以加上像 lg:block 这样的修饰符(或者任何你希望开始显示的断点),最终类名可能是:
class="hidden lg:block"有时候,你可能希望反过来:在小屏幕显示元素,但在大屏幕隐藏它。 比如,小屏幕使用汉堡菜单代替导航栏,而在大屏幕上显示完整导航栏,这时小屏幕行为是显示(默认), 大屏幕的隐藏行为则用断点修饰符实现,例如:
class="lg:hidden"同样的,常见做法是在小屏幕上把标题文字缩小。 小屏幕显示较小字号,所以 DOM 类名可以写成:
class="text-xl md:text-2xl lg:text-4xl"(关于如何避免重复输入,可以参考第 12 页的“Duplication”提示。)
小屏幕减少网格列数
从整体上看,响应式设计的目标之一是:在小屏幕上让信息垂直堆叠,而在大屏幕上充分利用空间水平排列。 具体实现方式取决于你的设计需求。
一种常见场景是:你有一组卡片式元素,比如新闻网站的精选文章,这些数据并不是表格,而是一系列横向排列的项目。
在这种情况下,你可能希望元素填满屏幕宽度,但每行显示的项目数量根据屏幕大小而变化:
- 在手机上,每行只显示 1 个
- 在桌面上,每行显示 4 个
可以这样写:
<div class="grid items-stretch
md:grid-cols-2 md:gap-4
lg:grid-cols-4 lg:gap-4">
<div class="mb-6 lg:mb-0"></div>
...
</div>这里发生了几件事:
- 父级
div在所有屏幕宽度下都是网格布局(grid)。 - 默认最窄屏幕下网格列数为 1,中屏幕时为 2 列,大屏幕时为 4 列。
items-stretch让每个子元素填满它所在的列宽度,也就是说随着屏幕变宽,元素会变大,直到下一个断点,再增加每行的元素数量。- 随着屏幕变大,网格间距(gap)也增大。
- 对于子元素,当每行只有一个元素时,使用
mb-6设置底部外边距,以保证间距;在大屏幕上,使用lg:mb-0取消底部外边距。
大屏幕使用 Flex 布局
另一种在不同屏幕尺寸下调整布局的方法是:在小屏幕上使用默认的块级间距(block spacing),在大屏幕上切换为 Flex 布局。
小屏幕上的块级间距可以保证元素保持纵向排列,即使某些元素较窄也不会影响布局; 大屏幕上的 Flex 布局则能让元素横向排列,均匀分布。
一个常见的应用场景是导航栏:
- 在大屏幕上,导航栏横向铺满页面顶部
- 在小屏幕上,导航栏变成竖向菜单列,通常会隐藏,直到点击菜单按钮才显示
这是一个简单示例:
这是一个简单示例:
<div class="w-full hidden lg:flex lg:flex-grow lg:items-center lg:w-auto
divide-black divide-y lg:divide-y-0"
id="navbar-menu">
<a class="block lg:mr-4 p-2 hover:bg-gray-200">Blog</a>
...
</div>说明:
- 外层
div在小屏幕上默认隐藏(hidden),通常配合 JavaScript 控制显示。 - 小屏幕显示时,它会使用默认的
block布局,即纵向列排列。 - 大屏幕上,
lg:flex会覆盖hidden,变为 Flex 布局并使用flex-grow,让子元素横向铺满屏幕。 - 小屏幕下,
divide-y会在子元素间添加竖直分隔线; 大屏幕上,lg:divide-y-0移除分隔线,让横向布局更清晰。 - 内层
<a>元素在大屏幕上有额外右边距(lg:mr-4),鼠标悬停时背景变灰(hover:bg-gray-200)。 <a>默认是行内元素,所以需要显式设置为block;如果用<div>作为子元素,则不需要加block。
要让这个导航栏正常工作,你需要一点 JavaScript。
下面的示例是纯原生 JavaScript(不依赖任何框架),假设你有三个元素:
- 导航菜单本身,对应前面讨论的外层
div,ID 为navbar-menu - 汉堡菜单按钮,ID 为
navbar-burger - 关闭按钮(“X”图标),ID 为
navbar-close,同样默认隐藏
这个脚本会在点击汉堡菜单时显示导航栏,并显示关闭按钮,同时隐藏汉堡菜单;点击关闭按钮则恢复初始状态。
<nav class="flex items-center font-bold text-grey=600">
<div class="block lg:hidden self-start">
<button id="navbar-burger" class="px-3 py-2 border rounded border-grey-400 hover:border-black">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<button id="navbar-close" class="px-3 py-2 border rounded border-grey-400 hover:border-black">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="w-full hidden lg:flex lg:flex-grow, lg:items-center lg:width-auto divide-black divide-y lg:divide-y-0" id="navbar-menu">
<a class="block lg:mr-4 p-2 hover:bg-gray-200">Blog</a>
AND SO ON
</div>
</nav>汉堡菜单和关闭按钮的 SVG 图标来自 Heroicons, 这是一个小型 SVG 图标库,由 Tailwind CSS 的开发团队提供。
接下来,我们给汉堡菜单和关闭按钮添加事件监听器:
- 点击汉堡菜单时:隐藏汉堡按钮,显示关闭按钮和导航菜单
- 点击关闭按钮时:隐藏关闭按钮和导航菜单,显示汉堡按钮
这可以用原生 JavaScript 实现,确保在小屏幕下导航栏可以通过按钮切换显示状态。
document.addEventListener('DOMContentLoaded', () => {
const $navbarBurger = document.querySelector('#navbar-burger')
const $navbarClose = document.querySelector('#navbar-close')
const $navbarMenu = document.querySelector('#navbar-menu')
$navbarBurger.addEventListener('click', () => {
$navbarMenu.classList.remove("hidden")
$navbarBurger.classList.add("hidden")
$navbarClose.classList.remove("hidden")
});
$navbarClose.addEventListener('click', () => {
$navbarMenu.classList.add("hidden")
$navbarBurger.classList.remove("hidden")
$navbarClose.classList.add("hidden")
});
})这样,你就有了一个基本的响应式导航系统。可以根据需要进行调整和美化。
接下来,我们将讨论如何自定义 Tailwind。