Better Icon and Label Alignment
Using the line-height (lh) CSS unit combined with flexbox and calc() solves icon misalignment in lists when text wraps across multiple lines.
Deep dive
- Use
display: flexwithalign-items: starton the container. - Define
--sizeas the icon width/height. - Apply
--offset: calc((1lh - var(--size)) / 2)as atranslateYtransform on the icon. - For pseudo-element icons, ensure
aspect-ratio: 1andflex-shrink: 0are set to prevent distortion. - Use
background-size: containto keep the icon contained within the pseudo-element bounds. - This approach handles text wrapping and dynamic font changes without breaking alignment.
Decoder
- lh unit: A CSS relative length unit that is equal to the computed value of the line-height property of the element on which it is used.
- Pseudo-element: A keyword added to a CSS selector that lets you style a specific part of the selected element(s), such as
::beforeor::after.
Original article
Better Icon and Label Alignment
There is a common design pattern that sometimes gets missed or overlooked. It’s having a list of items, each with an icon. When the text is one line, the icon is centered, but when the text has multiple lines, the icon is still centered but looks odd.
Throughout this article, I will show two solutions that I use based on the HTML:
- The icon is an HTML element (e.g.,
<svg>or<img>) - The icon is added via a pseudo-element as a background image
The icon in the markup
In this case, we have a container with the icon and the content. Usually, we use flexbox. Like so:
<ul class="list">
<li class="list-item">
<svg class="icon"></svg>
<span class="label">How to align an icon to a label.</span>
</li>
<!-- Other items -->
</ul>
.list-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
By default, it looks okay. However, when the list width is smaller and the text wraps, the icon will be centered. This is not the behavior we want.
How can we keep the icon aligned vertically when the text is wrapped? First, I went back to the basics. Instead of using align-items: center, I used start as a value.
.list-item {
display: flex;
align-items: start;
gap: 0.5rem;
}
But this caused an even bigger alignment issue. The highlighted area is the line height. The text element height has an empty space above and below it that comes from the font itself. When align-items: start is applied, the icon will be aligned to the top of the container.
What if we pushed the icon down to have it aligned with the top line?
.list-item {
display: flex;
align-items: start;
gap: 0.5rem;
}
.icon {
transform: translateY(3px);
}
That works. Let’s push the limits of it and see if it persists! Using a fixed value for the transform won’t work when the icon size, font size, or container width changes.
The solution: using the lh unit
If we can get the line-height value and we have a known icon size, we can make the offset relative to them.
.icon {
/* --icon-size is defined via JS. */
--size: var(--icon-size);
--offset: calc((1lh - var(--size)) / 2);
transform: translateY(var(--offset));
}
In this solution, if you change any of the three values, the alignment will just work. In some cases, the offset might be negative.
Icon as a pseudo-element
Sometimes, we don’t need to add the icon as an HTML element, so we use a pseudo-element for that.
Here is the basic CSS:
.list-item {
display: flex;
gap: 0.5rem;
}
.list-item::before {
content: "";
--size: var(--icon-size);
width: var(--size);
height: var(--size);
background-image: url("data:image/svg+xml,...");
background-repeat: no-repeat;
}
At first glance, it looks fine, right? Did you notice that the icon is cut off? This is because we need to limit the background size.
.list-item::before {
content: "";
--size: var(--icon-size);
width: var(--size);
height: var(--size);
background-image: url("data:image/svg+xml,...");
background-size: contain;
background-repeat: no-repeat;
}
If you increase the font size, the icon width shrinks because a flex item will shrink. We need to override that.
.list-item::before {
content: "";
--size: var(--icon-size);
flex-shrink: 0;
width: var(--size);
height: var(--size);
background-image: url("data:image/svg+xml,...");
background-size: contain;
background-repeat: no-repeat;
}
The solution
Currently, the list item is a flexbox container, and by default, the items stretch (vertically). I set the height property to auto.
.list-item::before {
content: "";
--size: var(--icon-size);
flex-shrink: 0;
width: var(--size);
height: auto;
background-image: url("data:image/svg+xml,...");
background-size: contain;
background-repeat: no-repeat;
}
To avoid icon distortion when the font size changes, we need to force the aspect ratio to a square.
.list-item::before {
content: "";
--size: var(--icon-size);
flex-shrink: 0;
aspect-ratio: 1;
width: var(--size);
height: auto;
background-image: url("data:image/svg+xml,...");
background-size: contain;
background-repeat: no-repeat;
}
With that, we will use the same technique as before and offset the pseudo-element by a few pixels.
.list-item::before {
content: "";
--size: var(--icon-size);
--offset: calc((1lh - var(--size)) / 2);
flex-shrink: 0;
aspect-ratio: 1;
width: var(--size);
height: auto;
background-image: url("data:image/svg+xml,...");
background-size: contain;
background-repeat: no-repeat;
transform: translateY(var(--offset));
}
The icon is now centered. It’s interesting to see what’s been used here: flexbox, aspect-ratio, the lh unit, calc(), and height: auto.
Outro
And that’s it. I solved a problem while working on a project and wanted to share it. Hope you’ve learned something new.
Thanks for reading.
Fresh dev news, AI-digested daily. Never miss a trend at devoured.fyi