CSS:如何加粗文本而不改变其容器的大小

208

我有一个水平导航菜单,基本上只是一个 <ul>,其中的元素并排设置。我没有定义宽度,而是使用填充,因为我希望宽度由菜单项的宽度定义。我加粗当前选定的项目。

问题在于加粗会使单词略微变宽,这会导致其余元素略微向左或向右移动。有没有巧妙的方法可以防止这种情况发生?例如告诉填充忽略加粗导致的额外宽度?我的第一个想法是从“活动”元素的填充中减去几个像素,但这个量是变化的。

如果可能,我想避免对每个条目设置静态宽度,然后居中,而是使用我目前拥有的填充解决方案,以便将来更改条目变得简单。


改变大小为什么是个问题?它是否会破坏布局? - Chaulky
我认为关于Web编程的问题最好在Stack Overflow上提问。也许管理员会将其迁移到那里。 - Ciaran
5
Chaulky:因为在布局方面,我最讨厌的事情之一就是那些你应该尝试点击的按钮会跳来跳去。 - Mala
我找到了一个解决方案,正是John和Blowski在讨论中提到的jscript。链接为http://doejo.com/blog/css-jquery-navigation-with-menu-items-enlarging-on-hover-without-shifting-adjacent-elements。 - thebulfrog
3
可能是在悬停时加粗的内联元素会移动的重复问题。 - Preview
以下是一个 JavaScript 解决方案,可用于水平和垂直菜单的干净渲染,无需移动菜单。只是晚了 11 年... :) 请参见 https://dev59.com/vm025IYBdhLWcg3w_rBA#73028356。 - EML
14个回答

226

我曾经遇到同样的问题,但通过一些妥协达到了类似的效果。我使用了text-shadow代替。

li:hover {text-shadow:0px 0px 1px black;}

这是一个可用的示例:

body {
  font-family: segoe ui;
}

ul li {
  display: inline-block;
  border-left: 1px solid silver;
  padding: 5px
}

.textshadow :hover {
  text-shadow: 0px 0px 1px black;
}

.textshadow-alt :hover {
  text-shadow: 1px 0px 0px black;
}

.bold :hover {
  font-weight: bold;
}
<ul class="textshadow">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li><code>text-shadow: 0px 0px 1px black;</code></li>
</ul>

<ul class="textshadow-alt">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li><code>text-shadow: 1px 0px 0px black;</code></li>
</ul>

<ul class="bold">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li><code>font-weight: bold;</code></li>
</ul>

jsfiddle 示例


70
我喜欢这个解决方案,尽管我建议将那一个翻转为1像素0像素0像素。看起来更加粗体。 - M. Herold
你真是个天才!简洁使它变得非常棒! - lufi
3
仅仅添加 li:hover {text-shadow: 1px 0 0 black;} 会导致文本字母之间的间距太小。为了再次改善这个问题,我们还需设置 li {letter-spacing: 1px;} - Patrick Hammer
9
我使用了“-0.5px 0 #fff, 0.5px 0 #fff”的代码,因为这可以让我们看到文字本身和阴影之间的小缝隙。此外,当文本加粗时,两侧都会增加重量。 - atorscho
5
我认为这不是一个好的解决方案,加粗字体不仅仅是宽度更大的字体,大多数字体在加粗时会有特殊的字形。 - Nikolai Mavrenkov
显示剩余4条评论

188

使用 ::after 的最佳工作方案

HTML

<li title="EXAMPLE TEXT">
  EXAMPLE TEXT
</li>

CSS

->

CSS

li::after {
  display: block;
  content: attr(title);
  font-weight: bold;
  height: 1px;
  color: transparent;
  overflow: hidden;
  visibility: hidden;
}

它通过使用title属性提供的粗体文本宽度,添加一个不可见的伪元素。

text-shadow解决方案在Mac上看起来不自然,并且没有充分利用Mac文本呈现所提供的所有美感。 :)

http://jsfiddle.net/85LbG/

来源:https://dev59.com/-3RB5IYBdhLWcg3wroyB#20249560


13
这是最可靠且“清洁”的解决方案,它只是在元素中保留了粗体文本的空间。在我的情况下,额外的伪元素导致高度改变。向::after块添加负margin-top解决了这个问题。 - Rudolf Gröhling
4
这个解决方案很棒!我使用了JS来添加一个“data-content”属性,其中包含元素的文本内容,这样我就可以避免改变HTML。 - mistykristie
4
太棒了。这应该是最好的答案。请注意,如果不是li元素,则可能需要在:after父元素上使用display:block;。 - Blackbam
2
当你被迫使用 title 属性,而且如果你没有 title 属性它甚至不能正常工作,这怎么能算是一个合适的解决方案呢? - codenamezero
2
最佳解决方案。另外一个补充:使用 height: 0; 是安全的。 - Tigran
显示剩余11条评论

59

经典 - 任何字体,广泛支持

最便携且视觉效果最佳的解决方案是使用text-shadow(感谢Thorgeir's answeratorscho's comment)-webkit-text-stroke-width(可用时。感谢Oliver's answer)相结合。

li:hover   { text-shadow: -0.06ex 0 0 currentColor, 0.06ex 0 0 currentColor; }
@supports (-webkit-text-stroke-width: 0.04ex) { /* 2017+, mobile 2022+ */
  li:hover { text-shadow: -0.03ex 0 0 currentColor, 0.03ex 0 0 currentColor;
             -webkit-text-stroke-width: 0.04ex; }
}

这会在每个字符的两侧使用当前字体颜色的微小“阴影”,并使用能够与字体渲染正确缩放的单位。如果有浏览器支持,那么该阴影就会减半,我们会增加用于绘制文本的笔画的宽度。这看起来会更加清晰,因为没有模糊半径的阴影是块状的,而在较高级别上笔画则是模糊的(请参见下面的演示)。

warning 警告: 虽然px值支持小数,但当字体大小改变时(例如用户使用Ctrl++缩放视图),它们看起来可能不太好。请改用相对值。

此答案使用ex单位的分数,因为它们随字体缩放而缩放。
在大多数浏览器默认情况下*,期望1ex8px,因此0.025ex0.1px

现代 - 可变字体

现在有一些新的可变字体,能够通过font-variation-settings改变字体等级。你需要两者支持:浏览器支持(自2018年以来非常好),以及特定字体的支持(这仍然很少见)。
@import url("https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght,GRAD@8..144,400,45;8..144,400,50;8..144,1000,0&family=Roboto+Serif:opsz,GRAD@8..144,71&display=swap");
li { font-family:Roboto Flex, sans-serif; }

/* Grade: Increase the typeface's relative weight/density */
@supports (font-variation-settings: 'GRAD' 150) {
  li:hover { font-variation-settings: 'GRAD' 150; }
}
/* Text Shadow: Failover for pre-2018 browsers */
@supports not (font-variation-settings: 'GRAD' 150) {
  li:hover { text-shadow: -0.06ex 0 0 currentColor, 0.06ex 0 0 currentColor; }
}

这会加载一个可变字体,然后在支持此类字体设置的浏览器中,在悬停时指示浏览器以粗体级别呈现该字体。传统解决方案(没有笔画,因为那些与可变字体支持一样新)作为旧版浏览器的备选项提供,但是自2018年以来,由于几乎所有浏览器都支持字体级别,因此不再需要。

为了完整起见(因为这会影响呈现宽度),可变字体还支持模拟重量(粗细),这与 font-weight 的预设重量形成对比。无论使用哪种方法,您都必须遵守字体的粒度。虽然支持 wght 变化的可变字体允许全范围的权重,但大多数字体要么缺少粗体变体,要么只有一个。需要呈现粗体字体的系统将根据需要自行完成,但仅在一个权重上(详细信息和示例在此处)。某些非可变字体提供几个权重,例如 Roboto,如下面的演示所示。在演示中玩转滑块以查看粒度差异。

演示比较六种方法

(不要被大型代码块吓到,这主要用于实现交互式滑块并比较回答此问题的所有方法。)

@import url("https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900");
@import url("https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght,GRAD@8..144,400,45;8..144,400,50;8..144,1000,0&family=Roboto+Serif:opsz,GRAD@8..144,71&display=swap");

body { font-family: 'Roboto'; }
.v   { font-family: 'Roboto Flex'; } /* variable! */

/* For parity w/ shadow, weight is 400+ (not 100+) & grade is 0+ (not -200+) */
li:hover { text-shadow: initial !important; font-weight: normal !important;
           -webkit-text-stroke-width: 0 !important; }
.weight  { font-weight: calc(var(--bold) * 500 + 400); }
.shadow, .grade { text-shadow: calc(var(--bold) * -0.06ex) 0 0 currentColor,
                               calc(var(--bold) * 0.06ex) 0 0 currentColor; }
ul[style*="--bold: 0;"] li { text-shadow:none; } /* none < zero */
.stroke   { -webkit-text-stroke-width: calc(var(--bold) * 0.08ex); }
.strokshd { -webkit-text-stroke-width: calc(var(--bold) * 0.04ex);
              text-shadow: calc(var(--bold) * -0.03ex) 0 0 currentColor,
                           calc(var(--bold) * 0.03ex) 0 0 currentColor; }

.after span   { display:inline-block; font-weight: bold; } /* @SlavaEremenko */
.after:hover span  { font-weight:normal; }
.after span::after { content: attr(title); font-weight: bold;
                     display: block; height: 0; overflow: hidden; }
.ltrsp        { letter-spacing:0px; font-weight:bold; } /* @Reactgular */
.ltrsp:hover  { letter-spacing:1px; }

@supports (font-variation-settings: 'GRAD' 150) { /* variable font support */
  :hover { font-variation-settings: 'GRAD' 0 !important; }
  .weight.v { font-weight: none !important;
    font-variation-settings: 'wght' calc(var(--bold) * 500 + 400); }
  .grade { text-shadow: none !important;
    font-variation-settings: 'GRAD' calc(var(--bold) * 150); }
}
Boldness: <input type="range" value="0.5" min="0" max="1.5" step="0.01"
            style="height: 1ex;"
            onmousemove="document.getElementById('dynamic').style
                           .setProperty('--bold', this.value)">

<ul style="--bold: 0.5; margin:0;" id="dynamic">
<li class=""        >MmmIii123 This tests regular weight/grade/shadow</li>
<li class="weight"  >MmmIii123 This tests the slider (weight)</li>
<li class="weight v">MmmIii123 This tests the slider (weight, variable)</li>
<li class="grade v" >MmmIii123 This tests the slider (grade, variable)</li>
<li class="shadow"  >MmmIii123 This tests the slider (shadow)</li>
<li class="stroke"  >MmmIii123 This tests the slider (stroke)</li>
<li class="strokshd">MmmIii123 This tests the slider (50/50 stroke/shadow)</li>
<li class="after"><span title="MmmIii123 This tests [title]"
                    >MmmIii123 This tests [title]</span> (@SlavaEremenko)</li>
<li class="ltrsp"   >MmmIii123 This tests ltrsp (@Reactgular)</li>
</ul>

将鼠标悬停在渲染的行上,以查看它们与标准文本的区别。 (这将颠倒问题的意图,使悬停文本加粗,以便我们更容易比较不同的方法。)移动“加粗度”滑块(对于滑块控制的条目)或更改浏览器的缩放级别( Ctrl + + 和 Ctrl + - )以查看它们如何变化。

请注意,非可变权重会在四个离散步骤中调整,而可变权重是连续的。

我添加了另外两个解决方案,供比较使用:@Reactgular的字母间距技巧,由于涉及猜测字体宽度范围,因此效果不佳,以及@SlavaEremenko的 ::after 绘图技巧,它留下尴尬的额外空间,以便粗体文本可以展开而不会推动相邻的文本项(我将归属放在粗体文本之后,以便您可以看到它不会移动)。


1
如此简单,如此美丽。 - Randy Hall
这个问题在 Firefox 82(我正在输入时)和 Safari 上存在。似乎使用 0.5px 是更好的选择。 - codenamezero
1
你可以使用 currentColor 替代 black 或任何特定颜色(尽管我还没有在许多浏览器上进行测试)。 - David Gilbertson
@DavidGilbertson - 很好的点子。currentColor普遍支持,所以我已经将其添加到代码中了。 - Adam Katz

28

我发现当你将字母间距调整1像素时,大多数字体的大小都是相同的。

a {
   letter-spacing: 1px;
}

a:hover {
   font-weight: bold;
   letter-spacing: 0px;
}

虽然这会改变常规字体,使每个字母之间多出一个像素的间隔。但是对于菜单标题来说,因为它们通常很短,所以不会有问题。


3
这是最好的、最被低估的解决方案,尽管我对它进行了更改,使其从0开始,并在加粗时变为负字间距。此外,您还需要对每种字体和字号进行微调。我还更新了@Thorgeir的小工具,将其包含在其中作为第二个解决方案。[jsfiddle.net/dJcPn/50/](http://jsfiddle.net/dJcPn/50/) - robotik
现代浏览器现在支持亚像素精度,因此您可以使用0.98px或更小的分数来微调间距。 - Reactgular
这看起来有点奇怪,因为间距算法并不完全相同(一些字母会晃动一下),更重要的是,即使将1px转换为相对值如0.025ex0.0125ex,当字体缩放时它也无法工作。请参见我的答案以进行实时演示。 - Adam Katz
@AdamKatz 我怀疑自此回答发布以来字体渲染已经发生了变化,而这很大程度上取决于你使用的字体。 - Reactgular

22

对于一个更加更新的答案,你可以使用-webkit-text-stroke-width

.element {
  font-weight: normal;
}

.element:hover {
  -webkit-text-stroke-width: 1px;
  -webkit-text-stroke-color: black;
}

这避免了任何伪元素(对于屏幕阅读器来说是个优点)和文本阴影(看起来很凌乱,仍然可能会产生轻微的“跳跃”效果),或设置任何固定宽度(可能不切实际)。

它还允许您将元素设置为粗体1像素以上(理论上,您可以使字体加粗至您喜欢的程度,并且也可以是创建没有粗体变体的字体的粗体版本的一种较差的锻炼方式,例如自定义字体 (编辑:可变字体取消了此建议)。尽管应该避免这样做,因为它可能会使一些字体看起来划痕和崎岖不平)

我确定这在Edge,Firefox,Chrome和Opera(发布时)以及Safari中都有效(编辑:@Lars Blumberg感谢您确认)。它在IE11或更早版本中不起作用。

还要注意,它使用了-webkit前缀,因此这不是标准的,未来可能会停止支持,因此如果粗体真的很重要,最好避免使用此技术,除非它仅仅是美学上的需要。


1
同样适用于Safari 13.0.5。到目前为止,对我来说是最干净的解决方案。 - Lars Blumberg
4
这在Firefox中可以工作,但请注意它可能看起来非常丑陋,有点模糊。 - MightyPork
1
不是一个好的解决方案,字体变得“更圆”,正如@MightyPork所提到的...模糊。 - codenamezero
1
2022年仍然没有很好的支持此功能:https://caniuse.com/?search=-webkit-text-stroke-width - Jonathan Rys
太棒了!我已将此添加到我的答案的演示中,并在检测到支持后,将我的text-shadow解决方案与此混合以减少@MightyPork指出的模糊。最新的解决方案是使用可变字体等级,这些字体在浏览器支持方面更好,但需要自定义字体(在我的答案中了解更多)。 - Adam Katz

9
我强烈建议尝试使用等宽字体来解决这个问题。

它们仍然是比例字体(而不是等宽字体),但它们在不同的字体粗细之间占据相同的大小。无需使用CSS、JS或花哨的技巧即可保持大小不变,因为这已经融入字体中了。

以下是一个来自这篇优秀文章的示例,比较了比例字体IBM Plex Sans和等宽字体Recursive。

comparing proportional to uniwidth font

您可以尝试的免费等宽字体包括:

该文章中还提供了许多非免费选项。


5
这是一个很老的问题,但我现在重新访问它,因为我在开发的应用程序中遇到了这个问题,并且发现这里提供的所有答案都不够好。我正在使用从cloud.typography.com获取的Gotham web字体,我的按钮一开始是空心的(白色边框/文本和透明背景),当鼠标移动到上面时就会获得背景颜色。我发现我使用的某些背景颜色与白色文本对比度不高,所以我想将这些按钮的文本更改为黑色,但是,无论是因为视觉技巧还是常见的抗锯齿方法,深色文本在浅色背景上始终似乎比白色文本轻。我发现将深色文本的重量从400增加到500可以保持几乎相同的“视觉”重量。但是,这会将按钮的宽度增加一个微小的量-一个像素的分数-但足以使按钮稍微“抖动”,而我想摆脱这种情况。
解决方案:
显然,这是一个非常棘手的问题,因此需要一个棘手的解决方案。最终,我遵循上面cgTag建议的方式,在更粗的文本上使用负的letter-spacing,但1px会过度,因此我计算了我需要的确切宽度。
通过在Chrome devtools中检查按钮,我发现我的按钮的默认宽度为165.47px,在悬停时为165.69px,差异为0.22px。该按钮有9个字符,所以:
0.22/9 = 0.024444px
通过将其转换为em单位,我可以使调整不依赖于字体大小。我的按钮使用了16px的字体大小,因此:
0.024444 / 16 = 0.001527em
因此,对于我特定的字体,以下CSS可以使按钮在悬停时完全保持相同的宽度:
.btn {
  font-weight: 400;
}

.btn:hover {
  font-weight: 500;
  letter-spacing: -0.001527em;
}

通过一些测试和使用上面的公式,您可以找到适合您情况的letter-spacing值,而且它应该适用于任何字体大小。

唯一需要注意的是不同的浏览器使用稍微不同的子像素计算,因此如果您想要达到这种强迫症级别的子像素完美精度,您需要重复测试并为每个浏览器设置不同的值。针对浏览器的CSS样式通常会受到反对,这是有道理的,但我认为这是唯一合理的选择。


这取决于使用的确切文本,因此如果使用多个按钮,则不适用于一般解决方案。 - lodewykk

4
很遗憾,避免文本加粗时宽度改变的唯一方法是定义列表项的宽度。但正如你所说,手动完成这个过程很费时间且不可扩展。我能想到的唯一办法是使用一些JavaScript来计算加粗前选项卡的宽度,然后在需要加粗的同时应用该宽度(无论是在鼠标悬停还是单击时)。

嗯,我会尝试一下这个,虽然我希望它至少对非JS用户看起来合理(即加粗)。也许我可以发送加粗的文本,使用JS取消加粗,计算宽度,然后再次加粗?或者这只是愚蠢的吗? - Mala
是的,这听起来像是照顾非JavaScript用户的最佳方式。 - ajcw

2
使用JavaScript根据未加粗内容设置li的固定宽度,然后应用样式到a标签(如果li没有子元素则添加span)来使内容加粗。

抱歉,重复了 :$ - Dan Blows
@Mala 你找到解决方案了吗?有趣的是,我也遇到了类似的问题。 - Dan Blows

0

我花了几个小时浏览这里的答案,但对于text-shadow-webkit-text-stroke-width解决方案、content:attr等的渲染质量不满意。

因此,这里提供了一个JS替代方案,当鼠标悬停或点击时显示干净的加粗和放大字体。垂直菜单版本在Codeply 这里,水平版本在这里。在两种情况下,注释掉对fixElementSize的调用以查看差异:使用调用,菜单项定位保持坚如磐石。

这个函数完成了艰苦的工作(对于水平版本,请将“height”替换为“width”),但它需要box-sizing:border-boxdisplay:block

/**
 * Set all anchor tags below 'myclass' to have the same fixed height.  Note
 * that 'getBoundingClientRect().height' gives the same integral result as
 * 'offsetHeight' for display:block
 */
function fixElementSize(myclass) {
    var atag1 = document.querySelector(myclass + ' a');
    var height = atag1.offsetHeight;
    var nlist = document.querySelectorAll(myclass + ' a');
    for (let i = 0; i < nlist.length; i++)
        nlist[i].style.height = height + 'px';
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接