使用原生 text-decoration 属性设置下划线、上划线和删除线的样式,而不是使用边框、背景渐变或伪元素来伪造它们。原生修饰会跟随文本跨越换行、跟踪字体指标、跳过字形下行字母,并通过嵌套元素继承,因此无需重新实现这些功能。只有在装饰必须勾勒或环绕文本(而不是沿文本延伸)时,才应使用本指南末尾介绍的基于框的技术。
文本修饰适用于任何文本,而不仅仅是链接。如果带下划线的文字不是链接,请确保其不会被误认为是链接。修饰链接时,请使用指向目标位置的真实 <a href>(添加 target="_blank" rel="noopener" 可打开新标签页),而不是使用带有脚本处理程序的 href="javascript:void(0)"。真实的链接会保留点击鼠标中键、复制链接和屏幕阅读器行为。
设置线条样式
使用 text-decoration 简写属性设置线条、样式、颜色和粗细,并使用 text-underline-offset 设置与文本的间距。将选择器封装在 :where() 中,这样默认值就具有零特异性,并且任何后续替换都会生效,而无需自定义属性。
:where(.decorated) {
/* Thickness is relative to the font size (0.08em is an example value), so the line is not too heavy
on small text or too light on large text, and max() keeps it at least 1px so it never disappears. */
text-decoration: underline solid currentColor max(1px, 0.08em);
/* Use em so the gap scales with the font size. 0.2em clears most descenders; example value. */
text-underline-offset: 0.2em;
}
/* Example hover state: a softer line that firms up on hover and focus. The 50% mix and
0.12em thickness are example values. */
:where(.decorated) {
text-decoration-color: color-mix(in srgb, currentColor, transparent);
}
:where(.decorated:is(:hover, :focus-visible)) {
text-decoration-color: currentColor;
text-decoration-thickness: max(1px, 0.12em);
}
/* Other lines are a different text-decoration-line on the same properties */
.decorated-over { text-decoration-line: overline; }
.decorated-through { text-decoration-line: line-through; }
text-decoration-style 接受 solid、double、dotted、dashed 和 wavy。text-underline-offset 仅适用于下划线。
设计起点:
- 粗细:正文约为
0.06em到0.1em。对于标题和强调状态,请使用较粗的字重(0.1em到0.15em)。 - 偏移:大约
0.15em到0.25em。虚线和波浪线比实线需要更多空间,因此请提高它们的偏移量。 - 颜色:
currentColor可使线条与文本保持一致。采用柔和的文字颜色,在悬停时颜色会加深,让长串链接看起来更舒适。 - 样式:
solid用于链接,dotted用于术语和缩写,wavy用于错误等注释。
控制跳过该行的位置
墨水断断续续
默认情况下,下划线和上划线会在与其相交的字形部分(例如下行字母)周围断开。设置 text-decoration-skip-ink 以选择行为:
.decorated {
/* auto: skip ink for most scripts (default). none: draw straight through.
all: also skip ink in scripts where auto does not, such as Japanese or Chinese. */
text-decoration-skip-ink: all;
}
- 对于拉丁文本,请保留
auto,并且不要声明它:它是默认值。 - 如果下划线距离基线足够近,会穿过表意字形,或者混合脚本文字必须看起来一致,请使用
all。 - 对于非常粗的线条或刻意装饰的线条,请使用
none,因为间隙看起来会像断点。
跳过空格
text-decoration-skip-spaces 决定是否在空格下绘制线条。默认值 start end 会使开头和结尾处的空格保持原样。
.decorated {
/* Spaces collapse by default, so there is nothing to skip. Preserve them when the
leading or trailing spaces are meaningful, such as padded input values. */
white-space: pre;
/* none: decorate every space. all: skip every space, so each word is underlined separately.
start | end | start end: skip only leading, trailing, or both. */
text-decoration-skip-spaces: all;
}
除非修饰应明显覆盖内边距 (none) 或在每个字词之间换行 (all),否则请勿声明 text-decoration-skip-spaces。普通的内嵌文本(例如链接文本)没有前导或尾随空格,因此该声明不会执行任何操作。
使用插图修剪或延长线条
text-decoration-inset 会移动线条的起点和终点。正值会将端点向内拉,负值会将端点向外推。一个值会同时移动两端;两个值会分别设置起点和终点。百分比是指修饰框的内嵌大小。
/* Small insets soften the ends of the line; larger ones read as a deliberately short line.
Both values are examples. */
.trimmed { text-decoration-inset: var(--deco-inset, 0.1em); }
.extended { text-decoration-inset: calc(var(--deco-inset, 0.1em) * -1); }
如需在悬停时显示下划线,而不显示边框或背景渐变,请将末端修剪 100%,然后将其过渡回 0:
.reveal {
text-decoration: underline; /* Always visible, including for reduced motion */
}
/* Only hide and animate the line when the user has not asked for reduced motion. */
@media (prefers-reduced-motion: no-preference) {
.reveal {
/* Trim the end by the full inline size so the line starts with zero length. */
text-decoration-inset: 0 100%;
/* 150ms to 300ms feels responsive without lagging; ease-out starts quickly. Example values. */
transition: text-decoration-inset var(--reveal-duration, 0.25s) ease-out;
}
/* Include :focus-visible so keyboard users get the same reveal as pointer users. */
.reveal:is(:hover, :focus-visible) {
text-decoration-inset: 0 0;
}
}
使用伪元素绘制自定义线条
如果线条必须是 text-decoration 无法绘制的形状(例如波浪线),请使用伪元素,并通过剪裁来显示它。无需额外标记,因此链接仍为纯文本 <a>。
<a class="draw" href="#">Hover to draw underline</a>
.draw {
position: relative;
display: inline-block;
color: inherit;
text-decoration: none;
padding-block-end: 0.4em; /* Space for the line below the text; example value */
}
.draw::after {
content: "";
position: absolute;
inset: auto 0 0 0;
block-size: 0.4em; /* Height of the squiggle; example value */
pointer-events: none;
border: var(--deco-thickness, max(1px, 0.08em)) solid var(--deco-color, currentColor);
/* A single shape strokes along its path, using the border width as the stroke. Example squiggle. */
border-shape: shape(from 0% 50%,
curve to 20% 50% with 5% 0% / 15% 100%,
smooth to 40% 50% with 35% 100%,
smooth to 60% 50% with 55% 100%,
smooth to 80% 50% with 75% 100%,
smooth to 100% 50% with 95% 100%);
}
/* Only hide and animate the line when the user has not asked for reduced motion;
otherwise it stays fully drawn. */
@media (prefers-reduced-motion: no-preference) {
.draw::after {
/* Clip the line away from the right edge. The negative block insets keep the stroke
from being cut off vertically. */
clip-path: inset(-1em 100% -1em 0);
/* A slightly slower draw than a hover reveal reads as drawing; example duration. */
transition: clip-path var(--draw-duration, 0.6s) ease-out;
}
.draw:is(:hover, :focus-visible)::after {
clip-path: inset(-1em 0 -1em 0); /* Fully drawn */
}
}
改为使用方框勾勒文本轮廓
两个框属性通过勾勒文本轮廓(而不是沿文本绘制线条)来装饰文本。为文本添加内边距,使轮廓不会接触到文本。
/* Gradient outline with a see-through middle */
.ring {
padding: 0.2em 0.75em; /* Example values: enough room that the outline does not touch the text */
/* The border width is the visible outline thickness. Relative to the font size, with a 2px
minimum so it stays visible on small text; example value. */
border: var(--ring-width, max(2px, 0.12em)) solid transparent;
background: linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777)) border-box;
/* Paint the background only under the border, so the inside stays transparent. */
background-clip: border-area;
}
/* Border that follows a shape that border-radius cannot draw, such as a tag with a pointed end */
.tag {
padding: 0.2em 1.5em 0.2em 0.9em; /* Example values: extra room at the point so the text is not clipped */
border: var(--shape-stroke, max(2px, 0.08em)) solid var(--shape-color, currentColor); /* Example stroke width */
/* A single shape strokes along its path. Two shapes fill the area between them.
This polygon is an example; use shape() for curves. */
border-shape: polygon(0 0, 85% 0, 100% 50%, 85% 100%, 0 100%);
}
border-shape 仅替换边框。背景仍然是矩形框,因此请勿依赖它来填充形状。
如果只有边角需要采用不同的形状(例如斜角、凹角或缺角标签),请改用 corner-shape 和 border-radius。它可让背景和边框遵循圆角,而不支持的浏览器会回退到普通圆角。
.label {
padding: 0.2em 0.9em; /* Example values */
border: var(--shape-stroke, max(2px, 0.08em)) solid var(--shape-color, currentColor);
border-radius: 0.6em; /* Corner size; example value */
corner-shape: bevel; /* round (default) | scoop | bevel | notch | square | squircle */
}
选择一种技巧
text-decoration(线条、墨水跳过、空格、内嵌):线条应沿文本延伸、随文本换行,并遵循文本的字体指标。这是默认选项。- 具有
border-shape和clip-path的伪元素:线条是自定义形状或需要绘制动画。只有在text-decoration-style无法产生所需外观时才选择它,因为它需要额外的定位。 background-clip: border-area:文本需要渐变轮廓。如果实线轮廓就足够了,请使用纯色border-color。corner-shape:只有边角需要不同的形状,例如斜角、凹角或缺口。将其与border-radius搭配使用,后者用于设置圆角大小。border-shape:轮廓需要border-radius无法绘制的形状,例如标记或箭头。如果圆角就足够了,请使用border-radius。- 悬停时显示内嵌内容:如果链接需要通过颜色变化以外的方式来提供悬停功能,则值得添加此功能。对于非互动文本,请跳过此步骤。
后备策略
每个后备字体都能确保文本可读,并保留核心装饰。根据您的基准目标来确定需求:如果目标包含不支持某个属性的浏览器,请应用相应的回退方案。
墨水跳跃 all
如果您的基准目标不支持 text-decoration-skip-ink: all,则系统会忽略该声明,并且浏览器会保留 auto。拉丁语下行字母仍会跳过;只有 auto 未涵盖的文字(例如日语)会失去墨水跳过功能。这是优雅降级,因此无需额外代码。
跳过空格
如果您的基准目标不支持 text-decoration-skip-spaces,浏览器会使用自己的默认空格处理方式。这就是优雅降级:在浏览器不会跳过的空格下方绘制下划线。
内凹
如果您的基准目标不支持 text-decoration-inset,则系统会忽略该声明,因此链接会保留其正常的、完全可见的下划线,并且过渡效果没有任何动画效果。这就是优雅降级:悬停显示效果消失,链接保持下划线。
边框区域
如果您的基准目标不支持 background-clip: border-area,则系统会忽略该声明,并在整个边框框下绘制背景,从而填充文本,而不仅仅是其轮廓。如果设计中包含渐变效果,请勿使用纯色边框。请改为重现渐变效果,并仅在 @supports 内应用 background-clip: border-area。
当元素的背景是已知的纯色时,请使用不透明层。在内边距框上绘制表面颜色,在边框框下绘制渐变,这样渐变就只会通过透明边框显示:
.ring {
border: var(--ring-width, max(2px, 0.12em)) solid transparent; /* Example width */
/* Fallback: an opaque surface layer hides the gradient inside the border. */
background:
linear-gradient(var(--surface, canvas) 0 100%) padding-box,
linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777)) border-box;
}
@supports (background-clip: border-area) {
.ring {
background: linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777)) border-box;
background-clip: border-area; /* The inside stays transparent */
}
}
如果内部必须保持透明,请使用带遮罩的伪元素。伪元素带有渐变,而遮罩会剪切除其内边距环以外的所有内容:
@supports not (background-clip: border-area) {
.ring {
position: relative;
border-color: transparent;
}
.ring::before {
content: "";
position: absolute;
inset: calc(var(--ring-width, max(2px, 0.12em)) * -1); /* Cover the border box */
padding: var(--ring-width, max(2px, 0.12em)); /* Ring thickness */
border-radius: inherit;
background: linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777));
/* Keep only the area between the content box and the border box. */
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
pointer-events: none;
}
}
边框形状
如果您的基准目标不支持 border-shape,则系统会忽略该声明,并且元素会保留其矩形边框。只有在形状纯粹是装饰性的情况下,圆角矩形才是可接受的后备方案。如果形状具有特定含义(例如标签、箭头或标注),请尽可能准确地再现该形状,并且仅在 @supports 内应用 border-shape。
使用经过剪裁的伪元素绘制轮廓。保留透明边框,使框的大小与支持的情况相同,用轮廓颜色填充伪元素,并使用 evenodd clip-path 将其切割成环形:外部形状,然后是按描边宽度内嵌的相同形状。
.tag {
--stroke: max(2px, 0.08em); /* Example stroke width */
position: relative;
padding: 0.2em 1.5em 0.2em 0.9em; /* Example values */
border: var(--stroke) solid transparent;
}
@supports not (border-shape: circle(50%)) {
.tag::before {
content: "";
position: absolute;
inset: calc(var(--stroke) * -1); /* Cover the border box */
background: var(--shape-color, currentColor);
pointer-events: none;
/* Outer shape, then the inner shape inset by the stroke. The inner points are approximate on
diagonal edges, so the stroke looks slightly thinner there. */
clip-path: polygon(evenodd,
0 0, 85% 0, 100% 50%, 85% 100%, 0 100%,
var(--stroke) var(--stroke),
calc(85% - var(--stroke)) var(--stroke),
calc(100% - var(--stroke) * 2) 50%,
calc(85% - var(--stroke)) calc(100% - var(--stroke)),
var(--stroke) calc(100% - var(--stroke)));
}
}
@supports (border-shape: circle(50%)) {
.tag {
border-color: var(--shape-color, currentColor);
border-shape: polygon(0 0, 85% 0, 100% 50%, 85% 100%, 0 100%);
}
}
如需绘制填充形状而非轮廓,请直接在元素上放置具有相同点的 clip-path: polygon(...)。
对于伪元素下划线,请在 .draw::after 上使用 border-block-end 绘制一条直线,并仅在 @supports (border-shape: circle(50%)) 内应用 block-size、border 和 border-shape 波浪线。线条是直的,但仍会绘制,因为 clip-path 动画不依赖于形状。
clip-path 还会剪裁焦点环和 box-shadow。请勿依赖于剪裁元素上的轮廓来显示焦点;而是通过颜色或背景更改来设置 :focus-visible 的样式。