Search code examples
csssassflexboxresize

Button with an icon and centered text


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.

Visual example of the buttons

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?


Solution

  • 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>


    Edit

    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>