开发者

CSS: bolding some text without changing its container's size

I have a horizontal navigation menu, which is basically just a <ul> with the elements set side-by-side. I do not define width, but simply use padding, because I would like the widths to be defined by the width of the menu item. I bold the currently-selected item.

The trouble is that in bolding, the word becomes slightly wider, which causes the rest of the elements to shift slightly to the left or right. Is there a clever way to prevent this from happening? Something along the lines of telling the padding to ignore the extra width caused by the bolding? My first thought was to simply subtract a few pixels from the padding of the "active" element, but this amount varies.

If possible I'd like to avoid setting a static width on 开发者_如何学运维each entry and then centering as opposed to the padding solution I currently have, in order to make future changes to the entries simple.


I had the same problem, but got a similar effect with a little compromise, I used text-shadow instead.

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

Here's a working example:

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 example


The best working solution using ::after

HTML

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

CSS

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

It adds an invisible pseudo-element with width of bold text, sourced by title attribute.

The text-shadow solution looks unnatural on Mac and doesn't utilize all the beauty that text rendering on Mac offers.. :)

http://jsfiddle.net/85LbG/

Credit: https://stackoverflow.com/a/20249560/5061744


Classic - any font, broad support

The most portable and visually pleasing solution would be to use text-shadow (h/t to Thorgeir's answer with atorscho's comment) combined with -webkit-text-stroke-width (when available. h/t to 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; }
}

This puts tiny "shadows" in your font's current color on both sides of each character using units that will scale properly with font rendering. If there's browser support, that shadow is halved and we increase the width of the strokes used to draw the text. This looks a little cleaner since shadows are blocky without a blur radius and strokes are blurry at higher levels (see the demo below).

CSS: bolding some text without changing its container's size

Warning: while px values support decimal values, they won't look so great when the font size changes (e.g. the user scales the view with Ctrl++). Use relative values instead.

This answer uses fractions of ex units since they scale with the font.
In ~most browser defaults*, expect 1ex8px and therefore 0.025ex0.1px.

Modern - variable fonts

There are now a few new variable fonts capable of things like changing font grade via font-variation-settings. You need both browser support (good since 2018) and support in the specific font you're using (which is still rare).

@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; }
}

This loads a variable font and then, in browsers that support such fonts' settings, instructs the browser to render that font with a bold-level grade when hovered. The classic solution (without strokes since those are about as new as variable font support) is provided as a failover for older browsers, but as there's rather universal support for font grade since 2018, that should no longer be necessary.

For completeness (since this does affect rendered width), variable fonts also support analog weights (boldness), which contrasts with presribed weights from font-weight. Using either method, you are beholden to the granularity of your font. While variable fonts that support the wght variation allow a full spectrum of weights, most fonts either lack a bold variation or else have only one. Systems presented with the need to render a font as bold will do it themselves as needed, but only at one weight (detail and example here). Some non-variable fonts offer several weights, like Roboto, used in the demo below. Play with the slider in the demo to see the granularity difference.

Demo comparing six methods

(Don't be daunted by the large code block, that's mostly used to implement the interactive slider and compare all of the methods offered by answers to this question.)

@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>

Hover over the rendered lines to see how they differ from standard text. (This reverses the question's intent to make hovered text bold so we can more easily compare the different methods.) Move the Boldness slider around (for the slider-controlled entries) or alter your browser's zoom level (Ctrl++ and Ctrl+-) to see how they vary.

Note how the (non-variable) weight adjusts in four discrete steps while the variable weight is continuous.

I added two other solutions here for comparison: @Reactgular's letter spacing trick, which doesn't work so well since it involves guessing font width ranges, and @SlavaEremenko's ::after drawing trick, which leaves awkward extra space so the bold text can expand without nudging neighboring text items (I put the attribution after the bold text so you can see how it does not move).


I found that most fonts are the same size when you adjust letter spacing by 1px.

a {
   letter-spacing: 1px;
}

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

While this does change the regular font so that each letter has an extra pixel spacing. For menus the titles are so short it doesn't present as a problem.


For a more up-to-date answer, you can use -webkit-text-stroke-width:

.element {
  font-weight: normal;
}

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

This avoids any pseudo-elements (which is a plus for screen readers) and text-shadows (which looks messy and can still create a slight 'jump' effect) or setting any fixed widths (which can be impractical).

It also allows you to set an element to be bolder than 1px (theoretically, you can make a font as bold as you like and could also be a shoddy-ish workout for creating a bold version of a font that doesn't have a bold variant, like custom fonts (edit: variable fonts depreciate this suggestion). Though this should be avoided as it will probably make some fonts appear scratchy and jagged)

I this definitely works in Edge, Firefox, Chrome and Opera (at time of posting) and in Safari (edit: @Lars Blumberg thanks for confirming that). It does NOT work in IE11 or below.

Also note, it uses the -webkit prefix, so this is not standard and support may be dropped in the future, so don't rely on this is bold is really important - it's best to avoid this technique unless it's merely aesthetic.


I would highly recommend trying a uniwidth font for this use case.

They're still proportional typefaces (as opposed to monospace), but they occupy the same size across different font weights. No CSS, JS, or fancy hacks are required to keep the size constant. It's baked right into the typeface.

Here's an example from this excellent article comparing the proportional font IBM Plex Sans to the uniwidth Recursive.

CSS: bolding some text without changing its container's size

Free uniwidth fonts you can try are:

  • Recursive
  • PT Root UI
  • Golos UI
  • Grandstander

There are also many non-free options linked to via the aforementioned article.


Unfortunately the only way to avoid the width changing when the text is bold is to define the width of the list item, however as you stated doing this manually is time consuming and not scalable.

The only thing I can think of is using some javascript that calculates the width of the tab before it is bold, and then applies the width at the same time the bold is required (either when you hover or click).


This is a very old question, but I'm revisiting it because I had this problem in an app I'm developing and found all of the answers here wanting.

(Skip this paragraph for the TL;DR...) I'm using the Gotham webfont from cloud.typography.com, and I have buttons which start hollow (with a white border/text and a transparent background) and acquire a background color on hover. I found that some of the background colors I was using didn't contrast well with the white text, so I wanted to change the text to black for those buttons, but — whether because of a visual trick or common anti-aliasing methods — dark text on a light background always appears to be lighter weight than white text on a dark background. I found that increasing the weight from 400 to 500 for the dark text maintained almost exactly the same "visual" weight. However, it was increasing the button width by a tiny amount — a fraction of a pixel — but it was enough to make the buttons appear to "jitter" slightly, which I wanted to get rid of.

Solution:

Obviously, this is a really finicky problem so it required a finicky solution. Ultimately I used a negative letter-spacing on the bolder text as cgTag recommended above, but 1px would have been way overkill, so I just calculated exactly the width I would need.

By inspecting the button in Chrome devtools, I found that the default width of my button was 165.47px, and 165.69px on hover, a difference of 0.22px. The button had 9 characters, so:

0.22 / 9 = 0.024444px

By converting that to em units I could make the adjustment font-size agnostic. My button was using a font size of 16px, so:

0.024444 / 16 = 0.001527em

So for my particular font, the following CSS keeps the buttons exactly the same width on hover:

.btn {
  font-weight: 400;
}

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

With a little testing and using the formula above, you can find exactly the right letter-spacing value for your situation, and it should work regardless of font size.

The one caveat is that different browsers use slightly different sub-pixel calculations, so if you're aiming for this OCD level of sub-pixel-perfect precision, you'll need to repeat the testing and set a different value for each browser. Browser-targeted CSS styles are generally frowned upon, for good reason, but I think this is one use case where it's the only option that makes sense.


Use JavaScript to set a fixed width of the li based on the unbolded content, then bold the content by applying a style to the <a> tag (or add a span if the <li> doesn't have any children).


You can implement this like amazon.com "Shop by department" hover menu. It uses wide div. You can create wide div and hide its right part


UPDATE: Had to use the B tag for the title because in IE11 the pseudo class i:after didn't show when i had visibility:hidden.

In my case I want to align a (custom designed) input checkbox/radio with label text where the text goes bold when the input is checked.

The solution provided here did not work for me in Chrome. The vertical alignment of input and label got messed up with the :after psuedo class and -margins did not fix this.

Here is a fix where you don't get trouble with vertical alignments.

/* checkbox and radiobutton */
label
{
    position: relative;
    display: inline-block;
    padding-left: 30px;
    line-height: 28px;
}

/* reserve space of bold text so that the container-size remains the same when the label text is set to bold when checked. */
label > input + b + i
{
    font-weight: bold;
    font-style: normal;
    visibility: hidden;
}

label > input:checked + b + i
{
    visibility: visible;
}

/* set title attribute of label > b */
label > input + b:after
{
    display: block;
    content: attr(title);
    font-weight: normal;
    position: absolute;
    left: 30px;
    top: -2px;
    visibility: visible;
}

label > input:checked + b:after
{
    display: none;
}

label > input[type="radio"],
label > input[type="checkbox"]
{
    position: absolute;
    visibility: hidden;
    left: 0px;
    margin: 0px;
    top: 50%;
    transform: translateY(-50%);
}

    label > input[type="radio"] + b,
    label > input[type="checkbox"]  + b
    {
        display: block;
        position: absolute;
        left: 0px;
        margin: 0px;
        top: 50%;
        transform: translateY(-50%);
        width: 24px;
        height: 24px;
        background-color: #00a1a6;
        border-radius: 3px;
    }

    label > input[type="radio"] + b
    {
        border-radius: 50%;
    }

    label > input:checked + b:before
    {
        display: inline-block;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%) rotate(45deg);
        content: '';
        border-width: 0px 3px 3px 0px;
        border-style: solid;
        border-color: #fff;
        width: 4px;
        height: 8px;
        border-radius: 0px;
    }

    label > input[type="checkbox"]:checked + b:before
    {
        transform: translate(-50%, -60%) rotate(45deg);
    }

    label > input[type="radio"]:checked + b:before
    {
        border-width: 0px;
        border-radius: 50%;
        width: 8px;
        height: 8px;
    }
<label><input checked="checked" type="checkbox"/><b title="Male"></b><i>Male</i></label>
<label><input type="checkbox"/><b title="Female"></b><i>Female</i></label>


I spent a couple of hours going through the answers here, but was unhappy with the rendering quality of the text-shadow and -webkit-text-stroke-width solutions, the content:attr, and so on.

So, here's a JS alternative that displays a clean bolded and enlarged font when hovered or clicked. The vertical menu version is on Codeply here, and the horizontal version is here. In both cases, comment out the call to fixElementSize to see the difference: with the call, the menu item positioning remains rock-solid.

The hard work is done by this function (substitute 'height' for 'width' for the horizontal version), but it requires box-sizing:border-box and display: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';
}


Interesting question. I suppose you are using float, right?

Well, I don't know any technique you can use to get rid of this font enlarging, hence they will try to fit in the minimum width required - and varying font thickness will change this value.

The unique solution I know to avoid this changing is one you said you don't want: setting fixed sizes to li's.


To offer another solution (since I wanted text shadow as well), I was able to do this with the following:

.menu {
  display: flex;
  justify-content: space-between;
  font-family: sans-serif;
}
.text-shadow {
  padding: 0 0.375rem;
}
.text-shadow:first-of-type,
.text-shadow:last-of-type {
  padding: 0 0.075rem;
}
.text-shadow:hover {
  text-shadow: 1px 1px white;
  font-weight: 900;
  padding: 0;
}
<nav class="menu">
  <div class="text-shadow">Item 1</div>
  <div class="text-shadow">Item 2</div>
  <div class="text-shadow">Item 3</div>
</nav>

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜