skip to Main Content

I am working on a React project where I need to animate individual letters in a string, so I am splitting the string into separate span elements for each letter. However, I’m facing an issue where the text looks different when split compared to when it is rendered normally as a continuous string.

Note: The text is split until character level, with each character wrapped with span.

Here is a simplified version of my code:

import { ReactNode } from "react";

export function splitWord(input: string): string[] {
  return input.match(/S+|s+/g) || [];
}

export function splitLetter(input: string): string[] {
  const characters: string[] = [];
  const regex = /[sS]/gu;
  let match;
  while ((match = regex.exec(input)) !== null) {
    characters.push(match[0]);
  }
  return characters;
}

export function splitStringWithSpan(input: string): ReactNode {
  return (
    <>
      {splitWord(input).map((wordOrSpace, wordIndex) => (
        <span
          className="inline-block [&>*]:overflow-y-clip [&>*]:inline-flex leading-tight"
          key={wordIndex}
        >
          {splitLetter(wordOrSpace).map((letter, letterIndex) => (
            <span key={letterIndex}>
              <span>{letter === " " ? "u00A0" : letter}</span>
            </span>
          ))}
        </span>
      ))}
    </>
  );
}

const Help = () => {
  return (
    <div className="text-5xl font-semibold flex flex-col">
      <div>
        <div className="text-xs">Splitted</div>
        {splitStringWithSpan("Young")}
      </div>
      <div>
        <div className="text-xs">Not Splitted</div>Young
      </div>
    </div>
  );
};

export default Help;

In the code, I render the text "Young" in two ways:

  1. Split into individual letters using splitStringWithSpan.
  2. Rendered as a normal string.

Issue:

example
The splitted text looks slightly wider from the non-split text. You can see there’s slightly different width at the character "Y" in the example.

Question:

How can I make the splitted text look the same as the non-split text in terms of character width and spacing?

Attempts:

I tried changing the max width of each individual character to
max-width: 0.94em;
It works for some character like "W", but still can’t handle all character’s spacing.

2

Answers


  1. There isn’t any extra width in the splitted text.

    The lack of the gap between the Y and the o in the non-splitted text is a result of kerning pairs in the font you are using. If you set a different font (through font-mono or similar), the rendering will become consistent between both versions.

    The horizontal spacing is disrupted in the splitted version because you set [&>*]:inline-flex on the individual letter spans, which forces each to be an individual container and prevents the kerning pairing from taking place. You can see this by either removing [&>*]:inline-flex, or by splitting on every second letter (which causes the first span to contain Yo together), either of which allow the kerning to happen properly (removing the extra space).

    Possible fixes – which is correct will depend on the context you are using this in:

    • remove [&>*]:inline-flex – not sure why it’s necessary here
    • use a different font without kerning pairs
    Login or Signup to reply.
  2. As explained by ThatNerd the spacing differences are caused by kerning (adding positive or negative offsets based on defined character sequences like Yo).

    Normalize to unkerned

    If you can live without kerning in the initial/unseparated state of the text just apply CSS font-kerning: none.

    kerning across multiple text nodes

    To my own surprise, modern browsers are pretty smart at applying kerning to multiple elements, as long as they have an inline display context. (Whereas splitting text nodes usually breaks ligatures – that’s why I’m surprised)

    @import 'https://fonts.googleapis.com/css2?family=Fira+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap';
    
    body{
      font-family: 'Fira Sans';
      font-size:5em;
    }
    
    .inl-blc{
      display:inline-block;
      outline: 1px solid red;
    }
    
    strong{
      margin:0;
      font-size:0.75rem;
    }
    p{
      margin:0;
    }
    
    .kerning{
        font-kerning: normal;
    }
    
    .nokerning{
        font-kerning: none;
    }
    <p class="kerning"><span class="inl-blc">Yo</span> <strong>Kerning applied</strong></p>
    <p class="nokerning"><span class="inl-blc">Yo</span> <strong>No Kerning applied</strong></p>
    <p class="kerning"><span class="inl-blc"><span class="Y">Y</span><span class="o">o</span></span> <strong>Split – kerning across spans</strong></p>

    In other words: for simple animations like a typewriter-effect (showing characters progressively) it might be enough to ensure all spans are in inline display mode.

    Emulate kerning?

    Unfortunately, once you apply another display value e.g inline-block or inline-flex – allowing individual margins or transformations – browsers will omit horizontal letter-spacing adjustments as defined by the font’s kerning pairs.
    Theoretically, you could write your custom kerning logic e.g by retrieving the font’s kerning data and writing your own CSS classes for certain character combinations/kerning-pairs. But frankly, this might be an overkill …

    @import 'https://fonts.googleapis.com/css2?family=Fira+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap';
    
    
    
    body{
      font-family: 'Fira Sans';
      font-size:10em;
    }
    
    .inl-blc{
      display:inline-block;
      outline: 1px solid red;
    }
    
    .inl-flx{
      display:inline-flex;
      outline: 1px solid red;
    }
    
    strong{
      margin:0;
      font-size:0.75rem;
    }
    p{
      margin:0;
    }
    
    .kerning{
        font-kerning: normal;
    }
    
    .nokerning{
        font-kerning: none;
    }
    
    /* CSS kerning pair */
    .Y+.o{
      margin-left:-0.06em;
    }
    <p class="kerning"><span class="inl-blc">Yo</span> <strong>Kerning applied</strong></p>
    <p class="kerning"><span class="inl-flx"><span>Y</span><span>o</span></span> <strong>Inline-flex – no kerning across spans</strong></p>
    <p class="kerning"><span class="inl-blc">Yo</span> <strong>Kerning applied</strong></p>
    <p class="kerning"><span class="inl-flx"><span class="Y">Y</span><span class="o">o</span></span> <strong>Inline-flex – manual kerning across spans</strong></p>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search