我做前端这些年被问得最多的一个“灵异现象”是给按钮写了background: linear-gradient(...)再配上transition: background 0.3s鼠标移上去颜色直接“啪”地跳过去一点过渡都没有。很多人第一反应是浏览器不支持、是 bug、是写法有问题然后开始翻文档、换all、加will-change全都没用。问题根本不在过渡在于你给background这个简写塞进去的东西本质上是css3 渐变而渐变属于image类型不属于color类型浏览器在默认情况下不会把两张渐变图当作可以插值的两个状态。这一条认知不打通后面所有的渐变写法都会别扭。这篇内容我打算把两件事揉在一起讲一是CSS3 渐变linear、radial、conic 三兄弟加上 repeating 变体的角度坐标系、色标规则和尺寸关键字二是文本格式化里那一堆被严重低估的属性——text-shadow、background-clip: text、text-decoration的子属性、white-space、word-break、overflow-wrap、text-overflow之间的关系网。适合已经能写 CSS、但总觉得“样式写着写着就玄学”的初中级前端也适合做设计还原时经常卡在细节上的同学。我会把每个属性“为什么长这样”讲清楚再给你能直接抄的组合写法。1. 渐变的真实身份是 image不是 color先把最根上的事情说透。CSS 里background这个简写属性其实同时控制background-color和background-image两套东西。background-color: red赋值的是一个颜色颜色是可以插值的——红到蓝之间浏览器知道怎么算出中间态。而background-image: linear-gradient(red, blue)赋值的是一个图像图像和图像之间浏览器默认不会去猜“这两张图怎么渐变到彼此”。1.1 为什么 transition 对渐变无动于衷这里有个很隐蔽的细节如果你写的是background-image: linear-gradient(...)那transition: background-image是无效的如果你写的是简写background: linear-gradient(...)transition: background会尝试去过渡但它能过渡的只有background-color那一部分background-image那一部分被直接跳过所以你看到的效果就是“瞬间切换”。这不是兼容问题是规范层面的行为。我第一次遇到这个问题时试图用transition: all 0.3s强行解决结果依然无效还额外引入了一堆意外过渡。后来才想明白all只是把“所有可过渡属性”都列了一遍background-image本来就不在可过渡的名单里。那想要渐变之间平滑变化业内通行的做法有三种各自的代价我列在下面方案写法要点优点代价双层叠加 opacity父元素放默认渐变伪元素放目标渐变只切换伪元素opacity兼容性最好性能可控需要额外一层DOM 结构要留位置property注册自定义属性声明--c1为color类型在渐变里引用它再对--c1做 transition写法干净纯 CSS需要较新的浏览器老环境要降级直接换 class 无过渡放弃动画接受硬切零成本视觉效果打折第二种展开说一下它是目前最“优雅”的路子property --grad-start { syntax: color; inherits: false; initial-value: #ff6b6b; } .btn { --grad-start: #ff6b6b; background-image: linear-gradient(120deg, var(--grad-start), #4ecdc4); transition: --grad-start 0.35s ease; } .btn:hover { --grad-start: #845ef7; }关键点在于property给--grad-start声明了明确的syntax: color。不声明的话自定义属性默认是“未解析的字符串”浏览器不知道它是颜色自然也没法插值。这个坑我在一个卡片项目里踩过整整一个下午写了个transition: --x 0.3s死活不动加上property立刻就好了。1.2 渐变写在 background 简写里的顺序陷阱还有一个新手高频翻车点background简写里background-color必须写在最后因为它只有这一个颜色值而渐变是 image。如果你写成/* 错误示范 */ .card { background: #fff linear-gradient(180deg, #f8f9fa, #fff); }浏览器解析时会因为#fff出现在 image 之前而直接判定整条声明无效整个背景消失。正确写法是.card { background: linear-gradient(180deg, #f8f9fa, #fff) #fff; }提示只要用了background简写background-size、background-repeat、background-position都会被重置成默认值。如果你在别处单独设置过它们简写会把它们统统冲掉。我的习惯是背景图相关一律拆开写只有纯色填充才用简写。2. linear-gradient 的角度坐标系与色标规则线性渐变的语法看着简单但角度和色标这两块几乎每个人都记反过一次。2.1 0deg 朝上这事和数学课本是反的CSS 的角度系统是这样的0deg 指向正上方90deg 指向正右方角度按顺时针递增。也就是说0degto top90degto right180degto bottom这是省略角度时的默认值270degto left为什么容易记混因为数学课本和很多图形库比如 Canvas 的arc里0 度指向正右方角度逆时针递增。两套坐标系完全不一样切过来的时候脑子必须手动转一下。我的记忆方式是把它想成钟表12 点方向是 0指针顺时针走3 点是 906 点是 1809 点是 270。还有一个更隐蔽的点用to关键字时角度到底是多少取决于盒子的宽高比。linear-gradient(to top right, red, blue)里的to top right并不是 45deg它是“从左上角指向右上角那个方向”如果盒子是 400×100 的横条这个方向大概是 14 度左右如果盒子是 100×400 的竖条就变成 76 度左右。所以你在一个宽盒子上调好的to top right渐变挪到方盒子上颜色分布会明显不同。需要“无论盒子什么形状渐变方向都稳定”时用角度值别用to关键字。这是我在做响应式卡片时的固定习惯。2.2 色标位置省略、乱序、双位置分别会发生什么色标color stop的完整写法是颜色 位置位置可以是百分比也可以写长度。这里有三个规则值得吃透规则一不写位置的色标会自动均分。linear-gradient(red, green, blue)等价于三个色标分别落在 0%、50%、100%。如果只写linear-gradient(red, green 60%, blue)那 red 在 0%、green 在 60%、blue 在 100%。浏览器会自动把第一个色标补到 0最后一个补到 100。规则二位置乱序时浏览器会自动抬升。你写linear-gradient(red 80%, green 20%)浏览器不会报错而是把 green 的位置抬到 80%结果就是 80% 之前全是 red之后全是 green中间没有过渡。这个行为在 conic-gradient 里做饼图时是核心技巧在线性渐变里则经常是“颜色怎么突然断了”的元凶。规则三双位置语法可以做出硬边。现代浏览器支持颜色 起点 终点的写法.stripes { background-image: linear-gradient( 90deg, #212529 0 25%, #f8f9fa 25% 50%, #212529 50% 75%, #f8f9fa 75% 100% ); }这四段会拼出清晰的竖条纹中间完全没有模糊过渡。以前要实现同样效果得写成#212529 25%, #f8f9fa 25%, #f8f9fa 50%, #212529 50%这种“同位置重复”的笨办法双位置语法把可读性拉回来了不少。关于颜色提示点还有个少见的用法在两个色标之间插入一个只有位置没有颜色的值用来控制中点位置。比如linear-gradient(red, 20%, blue)表示红蓝的中点被拉到 20% 的位置红色区域被压缩。做那种“大部分是浅色、边缘快速转深”的卡片底色时很好用。3. radial 与 conic尺寸关键字和饼图累加规则线性渐变之外的两个兄弟用法上坑更多因为它们的默认值经常不是你想要的。3.1 farthest-corner 这个默认值让渐变“没渐变完”径向渐变的完整语法是radial-gradient(形状 尺寸关键字 at 圆心位置, 色标...)。形状默认是ellipse椭圆尺寸关键字默认是farthest-corner最远的角。farthest-corner的意思是让渐变的边缘刚好碰到距离圆心最远的那个角。这意味着渐变的“100% 位置”通常落在盒子外面盒子里只能看到渐变的一段颜色往往“没走完全程”就结束了。这是很多人写radial-gradient(#fff, #000)却看不到完整黑白过渡的原因。四个尺寸关键字的行为对照关键字渐变边缘落在哪典型用途closest-side最近的边想要渐变在小盒子里完整走完closest-corner最近的角圆形头像的光晕farthest-side最远的边长条元素的背景光farthest-corner最远的角默认大面积的柔和扩散光实测下来做卡片高光、按钮内发光这类效果用closest-side更可控因为它的渐变范围始终在盒子内部不会“跑出去”。圆心位置用at控制at 30% 40%就是横向 30%、纵向 40% 的位置也可以写at top left、at center。写光斑效果时这个参数比尺寸关键字更关键光斑放偏一点整个卡片的质感就不一样。3.2 conic-gradient 画饼图的色标累加规则圆锥渐变的角度从 12 点方向开始顺时针递增这点跟线性渐变的“0 度朝上”是一致的。它的杀手锏是做饼图和数据环。饼图的标准写法是这样.pie { width: 160px; height: 160px; border-radius: 50%; background-image: conic-gradient( #ff6b6b 0 40%, #4ecdc4 0 70%, #845ef7 0 90%, #dee2e6 0 100% ); }注意中间那些0的写法。这里的规则是如果某个色标的起点小于前一个色标的终点浏览器会自动把它抬升到前一个终点的位置。所以第二段写#4ecdc4 0 70%那个 0 会被自动抬到 40%实际就是从 40% 到 70%。这样写的好处是不需要手动计算每一段的起点改数据时只改终点就行维护成本低很多。同样的规则也适用于conic-gradient做色轮、加载环、进度圈。做进度圈时可以用一个变量控制进度百分比.ring { --progress: 65%; border-radius: 50%; background-image: conic-gradient(#845ef7 var(--progress), #e9ecef 0); }这里#e9ecef 0的 0 会被抬到var(--progress)的位置正好衔接上一行搞定。3.3 repeating 系列为什么间距总是不对repeating-linear-gradient、repeating-radial-gradient、repeating-conic-gradient三个变体重复周期的计算逻辑是以最后一个色标的位置为周期长度。这听起来简单实际写起来经常翻车。问题出在“如果色标没写位置周期会是多少”。比如/* 周期不确定的写法 */ .bad { background-image: repeating-linear-gradient( 45deg, rgba(255, 255, 255, 0.06) 0, rgba(255, 255, 255, 0.06) 10px, transparent 10px, transparent 20px ); }这段是对的因为每个色标位置都写清楚了周期就是 20px。但如果有人图省事写成repeating-linear-gradient(45deg, #000, #fff)两个色标的位置是 0 和 100%那么“100%”指的是整个渐变区域的长度重复一次就铺满看不出条纹。所以写 repeating 系列的固定套路是每个色标都补上长度值最后一个色标的长度就是条纹周期。用双位置语法会更清楚.stripes-fixed { background-image: repeating-linear-gradient( 45deg, rgba(0, 0, 0, 0.05) 0 8px, transparent 8px 16px ); }16px 就是条纹间距改这个数字就能调密度不用动其他任何地方。4. 文本阴影、文字渐变与装饰线渐变讲完转文本这一块。文本格式化里被用得最多、也最容易用歪的是text-shadow和background-clip: text这两条链路。4.1 text-shadow 的多层叠加与描边替代方案text-shadow的语法是水平偏移 垂直偏移 模糊半径 颜色四值。多项之间用逗号分隔前面的阴影绘制在上层后面的在下层。这个顺序很重要做多层发光时如果顺序反了光晕会被压在文字底下看不见。一个常见需求是给文字加描边。CSS 有没有原生的文字描边有-webkit-text-stroke但它只支持-webkit-前缀而且它的描边是居中的会吃掉一部分字形细笔画会糊成一团。所以在实际项目里我更多用text-shadow的四向偏移来模拟描边.text-outline { color: #fff; text-shadow: 1px 0 0 #212529, -1px 0 0 #212529, 0 1px 0 #212529, 0 -1px 0 #212529; }这个方案的缺点很明显四个方向只能覆盖横竖斜角处会有缺口所以描边不能太粗。要做 2px 以上的描边四向就不够了得写八向甚至十六向代码会膨胀。这种时候我会退回-webkit-text-stroke接受它“吃字形”的代价然后再用paint-order: stroke fill调整绘制顺序.text-stroke-2 { -webkit-text-stroke: 2px #212529; paint-order: stroke fill; }paint-order让描边画在填充下面字形就不会被吃掉这是很多人不知道的一个细节。多层发光则是另一回事text-shadow堆叠三四层模糊半径依次增大.glow { color: #f8f9fa; text-shadow: 0 0 4px rgba(78, 205, 196, 0.8), 0 0 12px rgba(78, 205, 196, 0.5), 0 0 24px rgba(78, 205, 196, 0.3); }注意text-shadow的模糊半径和box-shadow的算法一致但阴影不占据布局空间也不会触发滚动条。阴影太重时性能会有感知尤其是在滚动列表里给每一项都加三层阴影。我的经验是列表项最多两层且模糊半径控制在 16px 以内。4.2 文字渐变background-clip 那条链路每一步都有坑文字渐变效果现在的标准写法是.gradient-title { background-image: linear-gradient(90deg, #ff6b6b 0%, #845ef7 100%); -webkit-background-clip: text; background-clip: text; color: transparent; }这一条链路上至少有四个坑我逐个说。坑一背景尺寸是按 padding box 计算的不是按文字范围。如果你的标题元素有左右 padding那渐变的起止点会覆盖到 padding 区域文字只占渐变中间的一段颜色分布跟你心里想的完全不一样。解决办法有两个给文字套一层display: inline-block的内层元素把渐变写在内层上或者用background-size手动校准。坑二color: transparent会继承。如果标题里有span强调词它会跟着变透明除非再给它单独设回颜色。用-webkit-text-fill-color: transparent更安全一些它不影响color的继承也不会破坏子元素的颜色.gradient-title { background-image: linear-gradient(90deg, #ff6b6b, #845ef7); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }坑三挑选中的文字看不见。因为填充色被设成透明用户框选时高亮区里看不到字。修复方式是给::selection单独设置.gradient-title::selection { background: #845ef7; -webkit-text-fill-color: #fff; }坑四background-clip: text在不支持的环境下会退化成什么。标准属性background-clip: text现在主流浏览器都支持了但如果是很老的环境它会静默失效结果是“整个文字块背后铺了一块渐变方块”视觉上非常难看。稳妥的降级办法是配合supportssupports (background-clip: text) or (-webkit-background-clip: text) { .gradient-title { background-image: linear-gradient(90deg, #ff6b6b, #845ef7); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } }4.3 text-decoration 的新增子属性text-decoration以前只有一个“简写 几个值”的粗放写法现在它已经拆成了一套子属性做下划线细节时非常好用子属性作用常用值text-decoration-line装饰线位置underline/overline/line-throughtext-decoration-color装饰线颜色任意颜色text-decoration-style装饰线形状solid/double/dotted/dashed/wavytext-decoration-thickness装饰线粗细1px/auto/from-fonttext-underline-offset下划线与文字的距离2px/0.2emtext-decoration-skip-ink下划线跳过字母下伸部auto/none其中text-underline-offset和text-decoration-thickness是我用得最多的两个。默认下划线离文字太近字母的降部比如 g、p、y 的下半截会跟线粘在一起加上text-underline-offset: 3px立刻就干净了。text-decoration-skip-ink: auto是默认值它会让下划线在遇到字母下伸部时自动断开视觉上更清爽但做那种“粗线贯穿”的强调效果时我会把它设成none让线连续不断地穿过去。还有一个不常用但很好玩的text-underline-position: under它把下划线放到文字降部以下的位置配合text-decoration-thickness做那种“设计感下划线”非常合适。5. 溢出、断行与省略号的三角关系这一块是 CSS 文本格式化里最容易出问题的地方因为涉及的属性名字太像功能互相交叠。5.1 单行省略那三件套缺一不可text-overflow: ellipsis本身什么都不做它只是一个“声明”——告诉浏览器“如果有溢出用省略号表示”。要让溢出真的发生必须同时满足三个条件.ellipsis { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }white-space: nowrap阻止文本换行让内容真的超出容器宽度overflow: hidden把超出部分裁掉text-overflow: ellipsis在这条裁切线上画上省略号少任何一个省略号都不会出现。我见过太多人只写了text-overflow: ellipsis然后纳闷为什么不生效。还有一个隐性条件容器必须有确定的宽度。如果容器在 flex 布局里父级又没约束宽度容器会被内容撑开永远不溢出省略号自然也不会出现。这种场景要给容器加min-width: 0或者overflow: hidden。多行省略是另一套写法用的是-webkit-box.clamp-2 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }它的坑在于display: -webkit-box会覆盖掉元素原本的display如果这个元素本来是个 flex 容器布局会整个塌掉。所以我一般会套一层外层管布局内层管文本截断。div classcard-title-wrap h3 classclamp-2一段很长的标题文字.../h3 /div.card-title-wrap { display: flex; align-items: center; } .clamp-2 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; min-width: 0; }5.2 white-space 六个值的实际差别white-space控制的是“空白字符空格、制表符、换行符怎么处理”和“能不能自动换行”两件事。六个值的组合方式用表格看最清楚值连续空格源码换行符自动换行normal合并成一个合并成一个空格允许nowrap合并成一个合并成一个空格禁止pre原样保留原样保留禁止pre-wrap原样保留原样保留允许pre-line合并成一个原样保留允许break-spaces原样保留原样保留允许行尾空格也保留pre和pre-wrap的差别只在换行pre会横向撑破容器pre-wrap会在容器边缘折行。做代码块展示时我用pre-wrap配合横向滚动作为兜底这样长命令既能折行又不会丢缩进。pre-line是个很实用的中间态它会保留源码里的换行符用户输入的换行会显示出来但会合并多余空格。做评论区、留言展示时用这个最合适——用户敲的回车有反应连续空格不会撑出一堆空白。break-spaces是后来补的值它跟pre-wrap的区别是行尾的空格不会被吃掉而是折到下一行占位。这个值的实际项目用途很少但在做等宽文本对齐时偶尔会用到。5.3 word-break 与 overflow-wrap 的分工这两个属性都能“让长单词换行”但作用机制完全不同混用会导致布局尺寸计算出现意外。word-break: break-all的规则是任意两个字符之间都允许断行包括英文单词中间的字母。它的副作用是会影响元素的min-content尺寸计算——因为任何位置都能断浏览器认为这个元素的最小宽度可以是“一个字符宽”于是 flex 布局里它会被压得非常窄。overflow-wrap: break-word的规则是优先按正常单词断行只有当整词放不下时才从中间断开。它不影响min-content的计算元素的最小宽度仍然是“最长的那个单词”的宽度。这个差异在 flex 布局里特别关键。我之前做过一个标签列表标签文字里有个特别长的英文标识符用word-break: break-all之后整个 flex 行被压扁标签全挤成一列查了半天才定位到这个属性上。换成overflow-wrap: break-word配合min-width: 0问题立刻消失。还有一个overflow-wrap: anywhere它跟break-word的区别是anywhere会在计算min-content时把“任意位置可断”考虑进去所以它确实会缩小元素的最小宽度。需要“强制容器可以缩得很窄”的场景用它其他场景用break-word。中文的断行默认规则跟英文不同中文任意两字之间都可以断所以中文长文本不需要word-break干预就能正常折行。但如果你想在中文环境下做“整词不断”可以用word-break: keep-all它会让 CJK 文本按词组断行——前提是文本里真的有空格或标点分隔否则会整段不换行撑出屏幕。6. 把渐变和文本属性拼起来的几个常用组合前面拆开讲了这节讲怎么拼。下面这几个组合是我在项目里反复复用的直接抄就能跑。6.1 渐变边框的三种写法对比做“渐变描边卡片”是高频需求。三种做法的实现和代价差别很大做法核心写法优点缺点border-imageborder: 1px solid transparentborder-image: linear-gradient(...) 1一行搞定圆角会失效永远直角双背景叠加background: linear-gradient(#fff,#fff) padding-box, linear-gradient(...) border-boxborder: 1px solid transparent支持圆角纯 CSS内层背景必须是不透明的实色伪元素 mask伪元素铺满父级渐变背景 mask挖空内部支持圆角、支持透明内层代码略长要注意层级第二种是我最常用的因为它代码最短又能保圆角.grad-border-card { border: 1px solid transparent; border-radius: 12px; background: linear-gradient(#ffffff, #ffffff) padding-box, linear-gradient(135deg, #ff6b6b, #845ef7) border-box; }原理是padding-box那层背景只铺在内边距区的内部border-box那层铺到边框外沿两层叠加后边框区域露出的就是渐变内部被白底盖住。所以内层背景必须写不透明色如果你想要半透明卡片这套方案就不行了得换第三种。第三种用mask做挖空.grad-border-mask { position: relative; border-radius: 12px; } .grad-border-mask::before { content: ; position: absolute; inset: 0; border-radius: inherit; padding: 1px; background: linear-gradient(135deg, #ff6b6b, #845ef7); -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; }这里padding: 1px决定了描边粗细mask-composite: exclude把内容区的遮罩挖掉只留下 1px 的环。这套写法支持透明内层也支持圆角代价是多一层伪元素和几行前缀属性。6.2 数字表格对齐与竖排文字表格里数字列不对齐是设计还原时最容易被挑出来的细节。默认字体里数字宽度不一致尤其是1比0窄很多一列数字排下来参差不齐。解决方案是使用等宽数字.num-cell { font-variant-numeric: tabular-nums; }这一行让所有数字占用相同宽度小数点自然对齐。如果你还要控制小数位数对齐那就得上font-feature-settings或者干脆用text-align: right配合固定宽度。竖排文字用writing-mode.vertical-label { writing-mode: vertical-rl; text-orientation: upright; letter-spacing: 2px; }vertical-rl是竖排从右往左符合传统中文阅读习惯。text-orientation: upright让拉丁字母也立起来默认是旋转 90 度。这里有个坑竖排之后width和height在视觉上像是交换了text-align控制的是纵向对齐margin的方向也跟着转。我第一次用的时候调了半天 padding就是因为思维还停留在横排。6.3 一条容易忘记的排版细节字体平滑最后补一个不属于渐变也不属于严格意义上的文本格式化但跟文字渲染质量直接相关的属性body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }这两行让 macOS 上的字体渲染从“次像素抗锯齿”切成“灰度抗锯齿”文字看起来会更细更清晰。代价是字重看起来会变轻如果你的设计里正文本来就用的是font-weight: 300加上这两行之后可能会细到看不清。所以我一般只在标题和按钮上开正文保持默认。做完这几块回头看最开始那个“渐变不能过渡”的问题其实答案一直很清楚搞清楚一个属性接受的数据类型是什么比记住它的语法重要得多。渐变是图像图像和图像之间默认不插值text-overflow是个声明声明背后需要overflow和white-space撑住word-break和overflow-wrap都能断词但一个改的是断行规则一个改的是溢出兜底。我在实际项目里踩过最久的一个坑不是上面任何一个属性本身而是把它们串起来时的相互覆盖——background简写冲掉了background-sizedisplay: -webkit-box冲掉了 flexcolor: transparent继承了子元素。所以现在的习惯是只要一个元素同时用了渐变和多行截断我一定先把它拆成两层结构外层管布局内层管视觉。多写一个div看起来笨但排查问题的时候能省掉半小时。7. 实测下来最值得记住的几条把上面这些压缩成几条能背下来的结论方便你下次写样式时快速对照渐变是image不是color所以transition: background对渐变无效要做渐变过渡就上property或者伪元素叠opacity。角度 0deg 朝上、顺时针递增to top right不是 45deg它的实际角度由盒子宽高比决定。radial-gradient的默认尺寸是farthest-corner渐变范围常常超出盒子想要完整过渡就用closest-side。conic-gradient里色标起点小于前一段终点时会自动抬升这是饼图不用手算起点的原因。repeating-*系列的周期等于最后一个色标的位置每个色标都要补长度值。text-overflow: ellipsis需要overflow: hiddenwhite-space: nowrap 确定宽度三件套。word-break: break-all会改变min-content计算flex 布局里优先用overflow-wrap: break-wordmin-width: 0。文字渐变用-webkit-text-fill-color: transparent比color: transparent安全别忘了补::selection的样式。最后再分享一个小技巧调试渐变时我会临时把background-size设成100% 100%并加一个outline: 1px solid red先把盒子的实际边界看清楚再判断渐变是“没走完”还是“超出去”了。很多时候颜色不对不是色标写错了是盒子的实际尺寸跟你以为的不一样尤其是元素被 flex 拉伸或者被box-sizing影响的时候。