I am working on a button component with an icon and text.
The icon is positioned on the left side of the button, and the text is centered within the button.
If the text cannot fit inside the button, it shouldn't wrap to a new line, but can be truncated.
The key condition for truncating the text is that the distance from the text to the edge of the button should be the same as the distance of the icon from the edge of the button.
So you can't add equal inline paddings to the button.
If the text is not truncated, it is centered within the button.
However, as soon as it starts overlapping the icon or doesn't fit inside the button, the text gets truncated.
In other words, in the normal state, the icon is positioned as is, but when there isn't enough space for the text, the icon pushes the text until it's truncated.
I have tried to achieve this with CSS, but failed.
I ended up adding a ResizeObserver to each button, which calculates whether there is enough space for the text.
If there isn't enough space, different styles are applied to the icon.
My solution with ResizeObserver, try to resize the window
CodePen
const CLASS_TEXT = `button__text`;
const CLASS_NO_FREE_SPACE = `button_no-free-space`;
const FREE_SPACE_SIZE = 70;
const buttons = document.querySelectorAll(`.${CLASS_ROOT}`);
const handleButtonResize = (entries) => {
entries.forEach((entry) => {
const { target } = entry;
const text = target.querySelector(`.${CLASS_TEXT}`);
const { width: widthButton } = entry.contentRect;
if (!(text instanceof HTMLElement)) {
return;
}
const widthText = text.offsetWidth;
const freeSpaceLeft = (widthButton - widthText) / 2;
const noFreeSpace = freeSpaceLeft <= FREE_SPACE_SIZE;
target.classList.toggle(CLASS_NO_FREE_SPACE, noFreeSpace);
});
};
const resizeObserver = new ResizeObserver(handleButtonResize);
[...buttons].forEach((button) => {
resizeObserver.observe(button);
});
My question is:
Is it possible to achieve the same effect using pure CSS?
Since the icons are always the same width (2em
), we can use an ::after
pseudo-element as a "buffer" for the right-side space balance.
Have the .button__icon
be in the flex layout flow. This will be crucial in "pushing" our other elements. Give it margin-right
to balance the left padding of the button.
Create an ::after
pseudo element that has flex-basis: calc(2em + 20px)
. The 2em
for the .button__icon
's width plus 20px
for the .button__icon
's margin-right
. This balances out the left and right space equally when .button__text
is short.
Apply justify-content: space-between
to the parent to aid in balancing out .button__icon
, .button__text
and ::after
when .button__text
is short.
Add flex-shrink: 999
, a huge shrink factor, so that the layout engine prioritizes the shrinking of the ::after
element when the .button__text
is longer.
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
padding: 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20px;
margin: auto 0;
}
.button {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
outline: none;
border: 1px solid #808080;
padding: 20px;
width: 100%;
background-color: transparent;
min-height: 80px;
}
.button::before {
content: "";
width: 1px;
position: absolute;
top: 0;
left: 50%;
bottom: 0;
background-color: red;
}
.button::after {
flex: 0 999 calc(2em + 20px);
content: "";
}
.button__icon {
flex-shrink: 0;
margin-right: 20px;
}
.button__text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
min-width: 0;
text-align: center;
border: 1px solid blue;
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"/>
<div class="grid">
<button class="button">
<i class="fa-solid fa-heart fa-2xl button__icon"></i>
<span class="button__text">
Long text that should be truncated
</span>
</button>
<button class="button">
<i class="fa-solid fa-thumbs-up fa-2xl button__icon"></i>
<span class="button__text">
Medium length
</span>
</button>
<button class="button">
<i class="fa-solid fa-house fa-2xl button__icon"></i>
<span class="button__text">
Short
</span>
</button>
</div>
To avoid .button__text
shrinking early, we could consider having the ::after
grow to calc(2em + 20px)
:
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
padding: 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20px;
margin: auto 0;
}
.button {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
outline: none;
border: 1px solid #808080;
padding: 20px;
width: 100%;
background-color: transparent;
min-height: 80px;
}
.button::before {
content: "";
width: 1px;
position: absolute;
top: 0;
left: 50%;
bottom: 0;
background-color: red;
}
.button::after {
flex-grow: 1;
max-width: calc(2em + 20px);
content: "";
}
.button__icon {
flex-shrink: 0;
margin-right: 20px;
}
.button__text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
min-width: 0;
text-align: center;
border: 1px solid blue;
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"/>
<div class="grid">
<button class="button">
<i class="fa-solid fa-heart fa-2xl button__icon"></i>
<span class="button__text">
Long text that should be truncated
</span>
</button>
<button class="button">
<i class="fa-solid fa-thumbs-up fa-2xl button__icon"></i>
<span class="button__text">
Medium length
</span>
</button>
<button class="button">
<i class="fa-solid fa-house fa-2xl button__icon"></i>
<span class="button__text">
Short
</span>
</button>
</div>