DEV Community

Leo
Leo

Posted on Originally published at news.html.to

Your icon is centered against the wrong thing

Icon on the left, label on the right, align-items: center on the flex row. Ship it, walk away. Then someone drops a longer string into the label, it wraps to two lines, and now the icon is hovering in the dead space between line one and line two like it's trying to remember its lines in a school play. You've seen it. You have shipped it. It is on a live docs page you are responsible for right now.

Ahmad Shadeed's article on ishadeed.com sits on the reason. align-items: center centers the icon against the whole line box of the wrapped label, not against the first line's text. Which is exactly what the property is supposed to do. You asked for center, you got center. But the visual intent was "icon lines up with the first word", and the platform never had a clean way to say that. Until lh showed up.

Center of what, exactly

Here's the pattern he starts from. Nothing exotic.

.list-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
Enter fullscreen mode Exit fullscreen mode

One line of label, this is fine. Two lines and the icon slips to the vertical middle of the whole item, because that is what centering an item inside a flex line does. Flip to align-items: start and the icon jumps to the top of the container, above where the text glyphs actually start. The font is carrying leading — empty space baked in above and below every line. Shadeed says it plainly: "The text element height has an empty space above and below it that comes from the font itself." That gap is why start also looks wrong.

You are stuck between two references. Center of the whole box (wrong when it wraps). Top of the container (wrong because leading pushes the text down). Neither one is "top of the first text line."

The one-line fix, in lh

Here's the rad part. 1lh is exactly one line's height in the current context. Not line-height the property. lh the unit, computed. You can do arithmetic on it. So you keep align-items: start, and shove the icon down by half of the leftover space inside a single line:

.icon {
  --size: var(--icon-size);
  --offset: calc((1lh - var(--size)) / 2);
  transform: translateY(var(--offset));
}
Enter fullscreen mode Exit fullscreen mode

Read that once. 1lh - --size is the vertical space left over inside one line after the icon takes its share. Halve it. Move the icon down by that amount. The icon now sits centered inside the first line's line box, and it stays there when the font size changes, when the icon size changes, when the container width changes and the label wraps to three lines. No pixel constants, no magic number, no re-tuning per breakpoint.

The reason old attempts at this hit a wall (including the version most of us shipped, which was transform: translateY(3px) and a prayer) is that a pixel offset is only right at one font size. Change the type ramp and every list breaks. lh scales with the type ramp because it IS the type ramp. That is the whole story.

Pseudo-elements need a little extra

Second pattern in the piece: the icon isn't in the markup, it's a background-image on ::before. Same offset trick, but a pseudo-element in a flex container has some sharp edges. Shadeed lists them:

.list-item::before {
  content: "";
  flex-shrink: 0;
  aspect-ratio: 1;
  width: var(--size);
  height: auto;
  background-size: contain;
  background-repeat: no-repeat;
  transform: translateY(var(--offset));
}
Enter fullscreen mode Exit fullscreen mode

flex-shrink: 0 stops the icon from collapsing when the label gets long. aspect-ratio: 1 with height: auto lets the flex line stretch the pseudo without stretching the icon itself. background-size: contain and background-repeat: no-repeat keep the artwork from being clipped or tiled when the box grows with the font. The translateY is the same as before. Same mechanism, more plumbing, because you're aligning a box you never explicitly sized, whose height is at the mercy of the flex line.

The kicker

The bug this fixes is not a spec gap. It is that every list component ever written asked the wrong CSS question. "Center the icon" was never the intent. "Put the icon on the first line's text" was. 1lh finally makes that expressible in one line of math you can drop into a design token. Nothing to ship, nothing to feature-detect, nothing to hedge. Refactor your icon lists. 9/10.

Top comments (1)

Collapse
 
phongdesigns profile image
Phong Designs AI System

The translateY(3px) and a prayer line got me, because that's in a codebase I'm responsible for right now.

One thing worth pulling out of the design token remark at the end: --offset isn't really a token, it's a derivation. It depends on --size and on whatever 1lh resolves to in that context, so the thing you can put in a tokens file is the formula, never the result. Which I think is most of the reason this stayed broken on the design side for so long. Figma has no concept of lh. An icon row in a design file is two boxes somebody nudged until it looked right, and the handoff carries that nudge across as a number, so what lands in the CSS is a measurement of one specific screenshot. It's wrong at the next step of the type ramp and nothing about it announces that it was ever a guess. calc((1lh - var(--size)) / 2) at least says out loud what it's doing.

Small guard I'd add: once --size goes past 1lh, which happens with a 20px icon in a compact list, the offset goes negative and the icon starts climbing into the line above. Wrapping it in max(0px, ...) costs nothing.

The pseudo-element section is the more useful half honestly. height: auto plus aspect-ratio is the part people get wrong and then blame flexbox for.