15

The issue seems to be that certain letters like g, y, q, etc. that have a tail that slopes downwards, do not allow for vertical centering. Here's an image to showcase the problem a.

The characters in the green box are basically perfect, as they have no downward tail. Those in the red box demonstrate the problem.

I would like for all characters to be perfectly vertically centered. In the image, characters with a downward tail are not vertically centered. Is this possible to rectify?

Here is the fiddle that demonstrates the problem in full.

.avatar {
    border-radius: 50%;
    display: inline-block;
    text-align: center;
    width: 125px;
    height: 125px;
    font-size: 60px;
    background-color: rgb(81, 75, 93);
    font-family: "Segoe UI";
    margin-bottom: 10px;
}

.character {
    position: relative;
    top: 50%;
    transform: translateY(-50%);
    line-height: 100%;
    color: #fff;
}
<div class="avatar">
  <div class="character">W</div>
</div>

<div class="avatar">
  <div class="character">y</div>
</div>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
Chron Bag
  • 587
  • 4
  • 17
  • 1
    This is an interesting question. The answer might be in here: https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align – dwjohnston Nov 18 '19 at 23:08
  • 2
    in this case you need to define what is the *center*. For me the *y* is actually centred and the *A* maybe not. let's not talk when we have different font-family, alignment will get even worse – Temani Afif Nov 18 '19 at 23:12
  • 1
    That's just how letters work. They are aligned, because the `v` in the `y` and `o` part in the `g` are on the same line as the lowest point for the capital letters. With your logic, Å, Ä, Ö would be aligned just as A and O but they can't be. If you want to do something special about it, you need to use javascript to check if it's a small-cap and then nudge the character up a few characters. – Rickard Elimää Nov 18 '19 at 23:12
  • 1
    I'm curious if there is a useful answer here. The problem would seem to be that that these genuinely are centered. ie. say you could move the y and the g up, what about if you had a lowercase a? how should that display? – dwjohnston Nov 18 '19 at 23:14
  • I'm open to using JavaScript if necessary. I'm looking for any way to have the content area of the letter be vertically aligned in the circle. – Chron Bag Nov 18 '19 at 23:14
  • I can accomplish something relevant with `vertical-align: middle` on an `inline-block` element, but it still isn't perfect (https://jsfiddle.net/yazbf5u3/). – Brilliand Nov 18 '19 at 23:19
  • 2
    Open your font in any font editor. Edit every character until you're happy with what you see as *"centered"*. Save and use as your brand new font-family. Should take not more than 15 min. I see no other sane way besides SVG or other trickeries using canvas & co. But I'll think about this question. – Roko C. Buljan Nov 18 '19 at 23:19
  • One idea I just had that might work is maybe drawing the letter onto an invisible canvas, cropping it, and then measuring the height, and then using that value to align it in the div. Not sure if that is overkill, if there is a better way, or if that would even work though. – Chron Bag Nov 18 '19 at 23:22
  • @RokoC.Buljan They're not centered by default for a reason. The font will be used everywhere and not just where it needs to be centered. In a typical capitalized textblock, all characters centered will look awful (don't know better word to describe it). – Anna Nov 18 '19 at 23:23
  • I would typically define height (rather than padding based height that we do sometimes) and write some `padding-bottom` to push it up. Only problem is this will break height if the defined height is too small. More hacks like a wrapper works. Where the text element (probably a span) will have position `relative` and get pushed up by `bottom` – Anna Nov 18 '19 at 23:30
  • You are required to post a [mcve] here, within your querstion, and never any third party site! – Rob Nov 18 '19 at 23:49
  • Can you convert all the characters to uppercase or do you need to use lowercase letters? – sissonb Nov 18 '19 at 23:52
  • 1
    This question is asking for exactly the same thing (but does not have a satisfactory solution): https://stackoverflow.com/questions/49839643/how-can-i-center-a-single-unicode-character-vertically-in-a-container – Brilliand Nov 19 '19 at 01:04

4 Answers4

6

Here is my solution using JS. The idea is to transform the element into an image in order to get its data as pixel then loop through them to find the top and bottom of each character and apply a translation to fix the alignment. This will work with dynamic font properties.

The code is not optimized but it highlight the main idea:

var elems = document.querySelectorAll(".avatar");

var fixes = [];


for (var i = 0; i < elems.length; i++) {
  var current = elems[i];
  domtoimage.toPixelData(current)
    .then(function(im) {
      /* Search for the top limit */
      var t = 0;
      for (var y = 0; y < current.scrollHeight; ++y) {
        for (var x = 0; x < current.scrollWidth; ++x) {
          var j = (4 * y * current.scrollHeight) + (4 * x);
          if (im[j] == 255 && im[j + 1] == 255 && im[j + 2] == 255) {
            t = y;
            break;
          }
        }
      }
      /* Search the bottom limit*/
      var b = 0;
      for (var y = (current.scrollHeight - 1); y >= 0; --y) {
        for (var x = (current.scrollWidth - 1); x >= 0; --x) {
          var j = (4 * y * current.scrollHeight) + (4 * x);
          if (im[j] == 255 && im[j + 1] == 255 && im[j + 2] == 255) {
            b = current.scrollHeight - y;
            break;
          }
        }
      }
      /* get the difference and apply a translation*/
      var diff = (b - t)/2;
      fixes.push(diff);
      /* we apply the translation when all are calculated*/
      if(fixes.length == elems.length) {
        for (var k = 0; k < elems.length; k++) {
          elems[k].querySelector('.character').style.transform = "translateY(" + fixes[k] + "px)";
        }
      }
    });
}
.avatar {
  border-radius: 50%;
  display: inline-flex;
  vertical-align:top;
  justify-content: center;
  align-items: center;
  width: 125px;
  height: 125px;
  font-size: 60px;
  background: 
    linear-gradient(red,red) center/100% 1px no-repeat,
    rgb(81, 75, 93);
  font-family: "Segoe UI";
  margin-bottom: 10px;
}

.character {
  color: #fff;
}
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/dom-to-image.js"></script>
<div class="avatar">
  <div class="character">W</div>
</div>

<div class="avatar">
  <div class="character">y</div>
</div>

<div class="avatar">
  <div class="character" style="font-size:35px">a</div>
</div>

<div class="avatar">
  <div class="character" style="font-size:25px">2</div>
</div>
<div class="avatar">
  <div class="character">o</div>
</div>
<div class="avatar">
  <div class="character">|</div>
</div>
<div class="avatar">
  <div class="character">@</div>
</div>
<div class="avatar">
  <div class="character">Â</div>
</div>

<div class="avatar">
  <div class="character" style="font-family:arial">Q</div>
</div>
<div class="avatar">
  <div class="character">~</div>
</div>
<div class="avatar">
  <div class="character">8</div>
</div>

<div class="avatar">
  <div class="character">ä</div>
</div>
<div class="avatar">
  <div class="character">ç</div>
</div>

<div class="avatar">
  <div class="character">$</div>
</div>

<div class="avatar">
  <div class="character">></div>
</div>
<div class="avatar">
  <div class="character">%</div>
</div>

UPDATE

Here is a first optimization of the code:

var elems = document.querySelectorAll(".avatar");
var k = 0;

for (var i = 0; i < elems.length; i++) {
  domtoimage.toPixelData(elems[i])
    .then(function(im) {
     var l = im.length;
      /* Search for the top limit */
      var t = 0;
      for (var j = 0; j < l; j+=4) {
          if (im[j+1] == 255) { /* Since we know the colors, we can only test the G composant */
            t = Math.ceil((j/4)/125);
            break;
          }
      }
      /* Search the bottom limit*/
      var b = 0;
      for (var j = l - 1; j >= 0; j-=4) {
          if (im[j+1] == 255) {
            b = 125 - Math.ceil((j/4)/125);
            break;
          }
      }
      /* get the difference and apply a translation*/
      elems[k].querySelector('.character').style.transform = "translateY(" + (b - t)/2 + "px)";
      k++;
    });
}
.avatar {
  border-radius: 50%;
  display: inline-flex;
  vertical-align:top;
  justify-content: center;
  align-items: center;
  width: 125px;
  height: 125px;
  font-size: 60px;
  background: 
    linear-gradient(red,red) center/100% 1px no-repeat,
    rgb(81, 75, 93);
  font-family: "Segoe UI";
  margin-bottom: 10px;
}

.character {
  color: #fff;
}
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/dom-to-image.js"></script>
<div class="avatar">
  <div class="character">W</div>
</div>

<div class="avatar">
  <div class="character">y</div>
</div>

<div class="avatar">
  <div class="character" style="font-size:35px">a</div>
</div>

<div class="avatar">
  <div class="character" style="font-size:25px">2</div>
</div>
<div class="avatar">
  <div class="character">o</div>
</div>
<div class="avatar">
  <div class="character">|</div>
</div>
<div class="avatar">
  <div class="character">@</div>
</div>
<div class="avatar">
  <div class="character">Â</div>
</div>

<div class="avatar">
  <div class="character" style="font-family:arial">Q</div>
</div>
<div class="avatar">
  <div class="character">~</div>
</div>
<div class="avatar">
  <div class="character">8</div>
</div>

<div class="avatar">
  <div class="character">ä</div>
</div>
<div class="avatar">
  <div class="character">ç</div>
</div>

<div class="avatar">
  <div class="character">$</div>
</div>

<div class="avatar">
  <div class="character">></div>
</div>
<div class="avatar">
  <div class="character">%</div>
</div>

I am using dom-to-image plugin for this.

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • A mite slow-running, but this is the first consistently-working solution so far. (With the caveat that the technically-centered  looks weird.) – Brilliand Nov 19 '19 at 00:44
  • 1
    @Brilliand I explicitely used `Â` to show that his centring isn't optimal as I commented in his answer ;) He will probably change his mind after seeing this and accept the way font works – Temani Afif Nov 19 '19 at 00:46
  • This looks pretty good. I think I'll use this, if I can make it more efficient. Currently it seems a bit slow. – Chron Bag Nov 19 '19 at 02:05
  • @ChronBag yes you can work on the code and optimise it, there is room for this. I didn't do it as it my take a lot of time and it's not really the purpose of the question. I wanted to focus on the main idea. Will probably do later by the way. – Temani Afif Nov 19 '19 at 07:52
  • @ChronBag added an optimization, a little faster I guess. – Temani Afif Nov 19 '19 at 15:57
1

Maybe there is a better answer, but it sounds like the only way to is to manually apply different styles depending on whether it is one of:

  • Capital letter
  • Lowercase with a tail
  • Lowercase with a stalk
  • Lowercase with neither

Now note that, in my understanding, the relative heights of tails and stalks I think is defined by the font. I'm not sure if there's a way to access that programatically - so you might need to adjust these values with the font.

Note also that this solution wouldn't work for supporting multiple languages - as you would need to define which category every single character fits in across dozens of different character sets.

const letters = ['a', 'b', 'y', 'X', 'c', 'y', 'A', 'B', 'Y']; 

function getAdditionalClass(char){
    //To do - fill arrays with the rest of the appropriate letters
    if (['y', 'g'].includes(char)) {
        return "tail"; 
    }
    if (['b', 'd'].includes(char)) {
        return "stalk"; 
    }
    
    if (['a', 'c'].includes(char)) {
        return "small"; 
    }
    
    return "capital"; 
}

letters.forEach(v => {
  const avatar = document.createElement("div"); 
  avatar.className = "avatar"; 
  const character = document.createElement("div");
  character.textContent = v; 
  character.className = `character ${getAdditionalClass(v)}`; 
  
  avatar.appendChild(character); 
  
  const root = document.getElementById("root"); 
  
  root.appendChild(avatar); 
  
});
.avatar {
    border-radius: 50%;
    display: block;
    text-align: center;
    width: 125px;
    height: 125px;
    font-size: 60px;
    background-color: rgb(81, 75, 93);
    font-family: "Segoe UI";
    margin-bottom: 10px;
}

.character {
    position: relative;
    transform: translateY(-50%);
    line-height: 100%;
    color: #fff;
}


.small {
    top: 45%; 
}

.stalk {
    top: 50%;
}

.tail {
    top: 41%;
}

.capital {
    top: 50%;
}

#root {
    display: flex; 
    flex-flow: row wrap; 
}
<div id = "root">

</div>
dwjohnston
  • 11,163
  • 32
  • 99
  • 194
0

This is a tricky situation!

From what I can tell, this will be most difficult to make natively scalable (i.e. %, vw or vh values instead of px or em). If you need to make this look pretty on mobile or tablet, please consider using my solution with @media breakpoints.

My solution essentially detects if it is a lowercase element with a tail and adds a class to offset the height to compensate for the tail.

In my testing, it didn't appear that any additional handlers were required for capital letters or lower case letters without a tail. Be feel free to correct me if I'm wrong.

There's a JSFiddle if you want to mess around and change / test this solution.

var circles = document.getElementsByClassName('circle');
var tails = ['q', 'y', 'p', 'g', 'j'] ;

Array.from(circles).forEach(render);
  
function render(element) {   
    if(element.innerText == element.innerText.toLowerCase() &&
     tails.includes(element.innerText)) {
     element.className += " scale";
    }
}
.circle {
  height: 150px;
  width: 150px;
  background-color: #bbb;
  border-radius: 50%;
  display: inline-block;
  text-align: center;
  vertical-align: middle;
  line-height: 150px;
  font-size: 50px;
}

.scale {
  line-height: 135px;
}
<div>
  <div class="circle">W</div>
  <div class="circle">X</div>
</div>
<div>
  <div class="circle">y</div>
  <div class="circle">t</div>
</div>

Let me know your thoughts and if I've missed anything. It'd be cool to get a final solution for this as I've had similar issues in the past myself!

EGC
  • 1,719
  • 1
  • 9
  • 20
  • This seems to be similar to dwjohnston's solution, and subject to the same pitfalls as his. Nevertheless, it is appreciated, and I might end up going with something along these lines if nothing more ideal comes along. – Chron Bag Nov 18 '19 at 23:48
  • I suppose as a last resort, you could go down the path of pixel counting - as seen [here](https://stackoverflow.com/a/16823769/11700321) whereby you would detect it's **actual height** in pixels and then apply the appropriate offset. – EGC Nov 18 '19 at 23:54
  • 1
    Yeah I mentioned that idea in the comments to the OP. Maybe that's the way to go. – Chron Bag Nov 18 '19 at 23:57
  • Sorry about noting your idea again.. too many comments, obviously missed it! – EGC Nov 19 '19 at 00:05
-1

You would probably need a helperclass for this, so that you can translate the lowercase letters more than the capital letters. A simple script can easily put these helper classes on automatically.

Hope this solves the problem for you :)

.avatar {
    border-radius: 50%;
    display: block;
    text-align: center;
    width: 125px;
    height: 125px;
    font-size: 60px;
    background-color: rgb(81, 75, 93);
    font-family: "Segoe UI";
    margin-bottom: 10px;
}

.character {
    position: relative;
    top: 50%;
    line-height: 100%;
    color: #fff;
}
.character-lowercase {
  transform: translateY(-60%);
}
.character-capital {
  transform: translateY(-50%);
}
<div class="avatar">
  <div class="character character-capital">W</div>
</div>

<div class="avatar">
  <div class="character character-lowercase">y</div>
</div>
someone
  • 57
  • 4
  • your `y` is not centered. Tweek some more! – Roko C. Buljan Nov 18 '19 at 23:28
  • I also don't know ahead of time what the letters will be. – Chron Bag Nov 18 '19 at 23:28
  • I understand that, but a script that checks if the letter is upper- or lowercase and puts a helperclass on the HTML based on that will fix that easily :) – someone Nov 18 '19 at 23:36
  • @RagnaRoaldset I think he doesn't mean upper or lower case. He meant there is lowercase yet different shape of character. Compare `o` and `y` and you'll get that. The characters with tails creates the problem in first place (mentioned in post). – Anna Nov 18 '19 at 23:39
  • So not a very great idea to detect each character and adjust them. – Anna Nov 18 '19 at 23:40
  • 1
    You could always create an array for all letters with tails, and loop through it to check if the letter in the div matches before applying the class :) But I get it, just tried to come with an solution :) hope someone comes up with it :) – someone Nov 18 '19 at 23:44