Skip to main content

Splitting text into individual characters with React

Posted on

Recently I needed to animate the individual characters in a heading element. I was hoping there would be a convenient css-only solution, similar to :nth-child(i) , but unfortunately that doesn't exist. So I decided to research how to achieve something similar, and accessible, nonetheless.

HTML

My first idea was to wrap each character in a separate <span> element manually.

<h1>
    <span>T</span>
    <span>e</span>
    <span>x</span>
    <span>t</span>
</h1>

However, there are two issues with this approach:

  1. Accessibility: by splitting the text up like this, screen readers would read each character individually, making it a painful experience for people relying on screen readers.
  2. Scalability: writing entire words or sentences out like that is an annoying process, that would have to be manually repeated each time, and doesn't work for text that's dynamically loaded.

An accessible and scalable solution with HTML and JavaScript

I found a solution on css-irl that takes care of both these issues, using aria elements for accessibility, and javascript to automate the text-splitting. It takes the text you want to split up as input, and returns it like this:

<h1 aria-label="Text">
    <span aria-hidden="true">T</span>
    <span aria-hidden="true">e</span>
    <span aria-hidden="true">x</span>
    <span aria-hidden="true">t</span>
</h1>

Screen readers will read the text defined inside aria-label but ignore the elements marked with aria-hidden="true". However, when I tried this with VoiceOver on Mac, I found I also had to add a role element to the parent in order for it to work.

<h1 aria-label="Text" role="heading"> ... </h1>

Since I do a lot of my work in React, I decided to create a similar solution inside a reusable component.

We know from the previous example that we have at least two pieces of variable information: the text that has to be displayed (this.props.copy) and the role of the element (this.props.role).

Based on that, we can start by creating a SplitText reusable component:

<SplitText copy="This is the text that will be split" role="heading" />

In the render function of our SplitText component, we first want to render one parent element, with aria-label={this.props.copy} and role={this.props.role}. This will make screen readers read the original text.

Then, we need to loop through the copy, and return each element wrapped in a span element with aria-hidden="true". This will visually render each character of the string, but screen readers will hop over it. We can loop through the text by turning it into an array, using the .split("") function.

render(){
    return(
        <span aria-label={this.props.copy} role={this.props.role}>
        {this.props.copy.split("").map(function(char, index){
            return <span aria-hidden="true" key={index}>{char}</span>;
        })}
        </span>
    );
}

Expanding on this

Now we that we have the basics in place, we can also expand on this logic, and add more functionality inside SplitText, for example custom class names or conditional styling. I will make a second tutorial, where we'll go more into depth and look at a couple of examples.

Hi! 👋🏻 I'm Sarah, an independent developer, designer and accessibility advocate, located in Oslo, Norway. You might have come across my photorealistic CSS drawings, my work around dataviz accessibility, or EthicalDesign.guide, a directory of learning resources and tools for creating more inclusive products. You can follow me on social media or through my newsletter or RSS feed.

💌 Have a freelance project for me or want to book me for a talk? Contact me through collab@fossheim.io.

If you like my work, consider:

Sign up for notifications and extra content

Subscribe to my newsletter to receive a notification when a new post goes live. I will also send occasional newsletter-only content about front-end development, accessibility and ethical/inclusive design.

You'll need to confirm your email address. Check your spam folder if you didn't receive the confirmation email.

Similar posts

View post archive