Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keep hanging characters visible in spite of `line-height: 1` and `overflow: hidden`

Tags:

html

css

Target

enter image description here

<div class="Card">
  <div class="Card-FullNameLabel">Gregg Sims</div>
  <div class="Card-OrganizationNameLabel">Compubotics</div>
</div>
  1. The .Card-FullNameLabel has font-size: 16px and line-height: 1.
  2. The .Card-OrganizationNameLabel has font-size: 12px and line-height: 1.
  3. The vertical space between .Card-FullNameLabel and .Card-OrganizationNameLabel must be exactly 6px.
  4. Below CSS rule must work and must NOT be changed.
.Card-FullNameLabel + .Card-OrganizationNameLabel {
  margin-top: 6px;
}
  1. Both .Card-FullNameLabel and .Card-OrganizationNameLabel must have overflow tolerance (e. g. if this content will be something like ÀÇĤjgpfhjklbĜiEstosTreMalfaci and so on it must not overhang from the parrent).
  2. All letters must be fully visible despite to line-height: 1.
  3. The mental arithmetic (magic numbers and/or hard coded offsets and other values which must be pre-computed) in CSS code are not allowed.

What is O'K to do: use the functionality of Pug pre-processor for markup and CSS pre-processors for styles.

🌎 Inital fiddle does not satisfied to the condition number 5: currently the card is not overflow-tolerant.

enter image description here

About line-height: 1, the bad practice

I has been repeatedly told about I must set line-height to value more than 1.

It becomes obvious that setting line-height: 1 is a bad practice. I remind you that unitless values are font-size relative, not content-area relative, and dealing with a virtual-area smaller than the content-area is the origin of many of our problems.

Deep dive CSS: font metrics, line-height and vertical-align

Well, I don't going to dispute about it. All I want is the working solution for the reaching of my target (descripted above). The usage of it is my responsibility and I will not reсcommend this solution if you agree that line-height must be more than 1.

But why I don't want increase the line-height so persistently?

Reason 1: The precise defining of the vertical space between two elements will become too complicated

The rule .Card-FullNameLabel + .Card-OrganizationNameLabel { margin-top: 6px; } is clear, intuitive and expresses the guidelines (represented in the picture above) by CSS. "The .Card-OrganizationNameLabel must retire from .Card-FullNameLabel by 6 pixels", and nothing more.

But what if we need to define the same vertical space between .Card-FullNameLabel and .Card-OrganizationNameLabel when line height is more than 1 (or they have the top and bottom paddings)? The value of the margin-top (visualized by non-overlayed pink area in the picture below) of .Card-FullNameLabel + .Card-OrganizationNameLabel rule now be the difference of:

  1. The desired range (6px)
  2. The extra vertical space below .Card-FullNameLabel (designated as l_b)
  3. The extra vertical space above .Card-OrganizationNameLabel (designated as l_a)

enter image description here

As I told above, the mental arithmetic is not allowed because it devalues the programming (CSS preprocessors capabilities in CSS case) and makes flexibility/maintainability impact (if we change the line-height or font-size or desired vertical space between labels, everything need to be mentally re-computed).

Although the preprocessor's variables (today became available in native CSS) can solve this problem, it will be too complicated to maintain it. To compute the non-intersecting red pink in the image above, we need to:

  1. Variablelize the font-size of .Card-FullNameLabel
  2. Variablelize the line-height of .Card-FullNameLabel
  3. Compute the extra space below .Card-FullNameLabel.
  4. Variablelize the font-size of .Card-OrganizationNameLabel
  5. Variablelize the line-height of .Card-OrganizationNameLabel
  6. Compute the extra space below .Card-OrganizationNameLabel
  7. Variablelize the desired range between .Card-FullNameLabel and .Card-OrganizationNameLabel (6 pixels in this example).

After this, we can finally compute the margin-top for the rule .Card-FullNameLabel + .Card-OrganizationNameLabel. And same for each pair of elements like .Card-FullNameLabel and .Card-OrganizationNameLabel!! Too poor technology for the web development in 2020s.

Reason 2: It does not require for each language

In below example, the Japanese symbols are perfectly fits to line with line-height: 1 (16px):

enter image description here

I suppose same will be for the Chinese, Korean and many other languages with non-latin characters.

But: in the small percentage of cases, there the foreign symbols could be mixed:

enter image description here

If to talk about high quality, this case must be supported.

I don't want increase the line height just for this exception. It's OK that the vertical space between lines actually became not 6px: the tails of j or À has a small weight and it will not break the geometric aesthetics.

My efforts

Attempt 1: usage of :before and :after

The SASS-mixin TextTruncation accepts the parameter $extraSpace which adding top and bottom paddings. The :before and :after pseudo elements compensates this paddings by negative margins.

@mixin TextTruncation($extraSpace, $displayEllipsis: false) {
  
  overflow: hidden;
  white-space: nowrap;
  
  @if ($displayEllipsis) {
    text-overflow: ellipsis;
  } @else {
    text-overflow: clip;
  }
  
  padding-top: $extraSpace;
  padding-bottom: $extraSpace;
  
  &:before,
  &:after {
    content: "";
    display: block;
  }
  
  &:before {
    margin-top: -$extraSpace;
  }
  
  &:after {
    margin-bottom: -$extraSpace;
  }
}

body {
  padding: 12px;
}

* {
  line-height: 1;
  font-family: Arial, sans-serif;
}

.Card {
  
  display: flex;
  flex-direction: column;
  align-items: center;
  
  width: 240px;
  height: 320px;
  padding: 6px 12px 12px;
  
  background: white;
  box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}

.Card-FullNameLabel {
  
  max-width: 100%; /* Required when the flex parent has `align-items: center` */
  @include TextTruncation($extraSpace: 2px, $displayEllipsis: true);
  
  font-size: 16px;
  color: #707070;
}

.Card-OrganizationNameLabel {
  
  max-width: 100%; /* Required when the flex parent has `align-items: center` */
  @include TextTruncation($extraSpace: 2px, $displayEllipsis: true);
  
  font-size: 12px;
  color: #A2A2A2;
}

.Card-FullNameLabel + .Card-OrganizationNameLabel {
  margin-top: 6px;
}

Unfortunately, It does not work: the effect is same as if no margins and no paddings has been defined:

enter image description here

🌎 CodePen

Attempt 2: usage of the wrapper

If the combination of overflow-x: hidden and overflow-y: visible works, it was the solution. But it does no work and this problem has been considered in the question CSS overflow-x: visible; and overflow-y: hidden; causing scrollbar issue.

I want to avid the wrappers as possible, but here it looks like the wrapper will be the last resort. To avoid of writing two tags each time, I created the Pug mixin:

mixin SingleLineLabel

  span.SingleLineLabel&attributes(attributes)
    span.SingleLineLabel-Text
      block

Well, the SingleLineLabel now a component. Besides the Pug mixin it's required to define the basic styles and SASS mixin allows to customize the label individually:

// Constant styles
.SingleLineLabel {

  overflow-y: visible;

  &:before,
  &:after {
    content: "";
    display: block;
  }


  &-Text {
    display: block;
    overflow-x: hidden;
    white-space: nowrap;
  }
}

// Variable styles
@mixin SingleLineLabel($truncatedVerticalSpaceCompensation, $displayEllipsis: false) {
  
  &:before {
    margin-top: -$truncatedVerticalSpaceCompensation
  }

  &:after {
    margin-bottom: -$truncatedVerticalSpaceCompensation
  }
  

  .SingleLineLabel-Text {
    
    padding-top: $truncatedVerticalSpaceCompensation;
    padding-bottom: $truncatedVerticalSpaceCompensation;
    
    @if ($displayEllipsis) {
      text-overflow: ellipsis;
    } @else {
      text-overflow: clip;
    }
  }
}

Now we can apply it:

.Card-FullNameLabel {
  
  max-width: 100%; /* Required when the flex parent has `align-items: center` */
  @include SingleLineLabel($truncatedVerticalSpaceCompensation: 1px, $displayEllipsis: true);
  
  font-size: 16px;
  color: #707070;
}

.Card-OrganizationNameLabel {
  
  max-width: 100%; /* Required when the flex parent has `align-items: center` */
  @include SingleLineLabel($truncatedVerticalSpaceCompensation: 2px, $displayEllipsis: true);
  
  font-size: 12px;
  color: #A2A2A2;
}

.Card-FullNameLabel + .Card-OrganizationNameLabel {
  margin-top: 6px;
}

It seems like the target has been reached:

enter image description here

🌎 CodePen

Unfortunately, it has the bug which occurrence regularity is unclear. Sometimes the small vertical scrollbar appearing.

enter image description here

I really don't know how to reproduce it, but in the past experiment it has occurred, for example, if to switch the browser to device simulation mode by development tools and then exit from this mode. Most likely, you will not get the same effect if repeat same experiment in fiddle.

Finally

The solution based on your great answers will be included to growing @yamato-daiwa/frontend library.

If you have the full list of the problematic symbols like g, p, À, Ĥ and so on, please share it - I'll use it for the tests and also add them to the future pug functionality for the overflow tolerance testing.

like image 406
Takeshi Tokugawa YD Avatar asked Aug 05 '21 13:08

Takeshi Tokugawa YD


People also ask

What does overflow hidden mean?

Set Overflow to hidden to prevent unwanted horizontal scrolling and extra whitespace in your project when elements exist outside the viewport.

Are leading and line height the same thing?

Leading, a.k.a. line-height in digital circles, is the space between baselines in a block of text. The nuts and bolts: The gap between adjacent lines of type in typography is known as leading. In CSS, the line-height property specifies the amount of space between inline elements.

How do you increase line height in CSS?

The line-height CSS property sets the height of a line box. It's commonly used to set the distance between lines of text. On block-level elements, it specifies the minimum height of line boxes within the element. On non-replaced inline elements, it specifies the height that is used to calculate line box height.

What does overflow visible mean?

overflow: visible By default, the overflow is visible , meaning that it is not clipped and it renders outside the element's box: You can use the overflow property when you want to have better control of the layout. The overflow property specifies what happens if content overflows an element's box.

Can I set the topmost element’s overflow to hidden?

You might wonder if you can set the topmost element’s Overflow to hidden — the Body element. If you set Overflow to hidden on the Body, this can clip the rest of the content on the page and stop users from being able to successfully scroll through your page beyond what’s initially visible in the viewport.

What does it mean when an element is hidden?

When you set Overflow to hidden, this means you’re hiding elements that spill outside the parent element’s boundary. Any element that spills outside the parent element’s boundary (pictured as a dark rectangle around the page content) no longer appears after setting the parent element to Overflow: hidden.

What is the overflow property in HTML?

By default, the overflow is visible, meaning that it is not clipped and it renders outside the element's box: You can use the overflow property when you want to have better control of the layout. The overflow property specifies what happens if content overflows an element's box.

What does x height mean in text?

X-height: This is (unsurprisingly) the height of a normal, lowercase x in a line of text. Generally, this is the height of other lowercase letters, although some may have parts of their characters that will exceed the x-height.


1 Answers

I know you explicitly stated that you needed to keep line-height: 1 and margin-top: 6px, but as you identified with the documented overflow CSS issue, you're kind of stuck with your current restrictions.

If it is at all possible to be flexible about those restrictions, I have a solution that is visually identical to what you were originally after.

Original State

I started with your Initial Fiddle and added ellipsis truncation CSS and problematic text in the html.

.Card-FullNameLabel,
.Card-OrganizationNameLabel {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

The result is this state which I am calling "Original" since it leaves your line-height and margin values unmodified. Note that I have added a single overflow: hidden rule in place of the problematic mixing of overflow-x and overflow-y rules.

Proposed Fix

I propose the following CSS changes. This increases line-height to 1.5 which allows all of the font's ascenders and descenders to be visible. Then I added negative offset margins to compensate:

.Card-FullNameLabel,
.Card-OrganizationNameLabel {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;

  /* Shows all ascenders and descenders */
  line-height: 1.5;
}

.Card-FullNameLabel {
  font-size: 16px;
  color: #707070;

  /* Compensates for line-height */
  margin: -4px 0;
}

.Card-OrganizationNameLabel {
  font-size: 12px;
  color: #A2A2A2;

  /* Compensates for line-height */
  margin: -3px 0;
}

.Card-FullNameLabel + .Card-OrganizationNameLabel {
  /* 6px visually (minus 3px) */
  margin-top: 3px;
}

The result can be seen in action here which I am referring to as "Proposed Fix". I have confirmed the results are consistent in latest desktop Chrome, Firefox, and Safari on MacOS and Mobile Safari on iOS.

Comparison

I made a simple animation from "before" and "after" screenshots that demonstrate that the output is visually identical except that the proposed fix does not cut off the font's ascenders and descenders.

Note that you can click the animation to see a full-size, 1:1 pixel-accurate version.

enter image description here

I did some additional tests with what I'm calling "in-between elements" to demonstrate that the proposed fix would behave the same as the original even if there were elements in-between.

enter image description here


Update: Automation

As was made clear in the comments, one of the requirements is that there be no "hard-coded" or "magic" numbers in the CSS. So while the above solution works, it requires manual arithmetic ahead of time.

Here is an updated Codepen that can automatically produce similar CSS to what was shown above by using some SCSS logic that will calculate the offsets based on these input values:

Variable Current Value
$globalLineHeight 1
$minLineHeight 1.5
$fullNameFontSize 16px
$fullNameLineHeight $globalLineHeight
$orgNameFontSize 12px
$orgNameLineHeight $globalLineHeight
$orgNameMarginTop 6px

For demonstration purposes, I added a bit of extra code that will show a "before" and "after" hover effect so you can see how the SCSS logic behaves compared to the original CSS. As is indicated in both the HTML and CSS, you can delete anything below the lines that begin with #DELETE-ME.

like image 179
Aaron Sarnat Avatar answered Nov 15 '22 04:11

Aaron Sarnat