The Cost of Accessibility in React: What You Need to Know

by Hannah Goodridge ~ 6 min read

Like this post?

00

During my time as a frontend developer, I've come to appreciate the importance of accessibility in web development. Making sure our applications are inclusive and usable by everyone is crucial. But did you know that accessibility can sometimes come at a cost? 🤔

The Beauty and the Beast of Conditional Rendering

One of the most powerful features in React is the ability to conditionally render components. It's super useful, right? You click a tab, and voilà, the content appears without anything unnecessary cluttering the DOM. However, there's a beast lurking behind this beauty: accessibility issues.

Let's take a simple tab component as an example:

import React, { useState } from 'react'; function TabComponent() { const [activeTab, setActiveTab] = useState(0); return ( <div> <button onClick={() => setActiveTab(0)}>Tab 1</button> <button onClick={() => setActiveTab(1)}>Tab 2</button> <div role="tabpanel" hidden={activeTab !== 0}> <p>Content for Tab 1</p> </div> <div role="tabpanel" hidden={activeTab !== 1}> <p>Content for Tab 2</p> </div> </div> ); } export default TabComponent;

Here, we're using the hidden attribute to conditionally hide and show tab content. While this works beautifully for immediate responsiveness, screen readers might not be too happy. 😕

Accessibility Concerns

Screen readers typically ignore elements with hidden attributes, which means users relying on them won't be able to access the tab content. This isn't cool because everyone deserves a seamless web experience, right?

To combat this, we can utilize aria attributes more effectively:

import React, { useState } from 'react'; function AccessibleTabComponent() { const [activeTab, setActiveTab] = useState(0); return ( <div> <button role="tab" aria-selected={activeTab === 0} onClick={() => setActiveTab(0)} id="tab1" aria-controls="tabpanel1" > Tab 1 </button> <button role="tab" aria-selected={activeTab === 1} onClick={() => setActiveTab(1)} id="tab2" aria-controls="tabpanel2" > Tab 2 </button> <div role="tabpanel" aria-labelledby="tab1" aria-hidden={activeTab !== 0} hidden={activeTab !== 0} id="tabpanel1" > <p>Content for Tab 1</p> </div> <div role="tabpanel" aria-labelledby="tab2" aria-hidden={activeTab !== 1} hidden={activeTab !== 1} id="tabpanel2" > <p>Content for Tab 2</p> </div> </div> ); } export default AccessibleTabComponent;

Adding aria attributes like aria-selected, aria-controls, and aria-labelledby helps screen readers understand the relationship between tabs and their content. This way, users can navigate through the tabs and access the content seamlessly.

SEO Considerations

Now, shifting gears a bit – let's talk about SEO. When React conditionally renders content (especially if it's not rendering it at all at first), that content might not be seen by search engine crawlers. This can take a toll on your SEO since those juicy keywords and backlinks sitting in the hidden content are going unnoticed.

Search engines like Google are getting better at crawling JavaScript, but relying solely on their advanced capabilities can be risky. Consider server-side rendering (SSR) with frameworks like Next.js, which sends fully rendered HTML to the client. This way, your content is always visible to search engines regardless of dynamic client-side rendering.

Performance Trade-Offs

Now, let's talk about another biggie: performance. Conditionally rendering components can significantly boost performance by reducing what needs to be processed and displayed at any given time. But this comes at the cost of accessibility and SEO.

When to Render All Content: Acceleration Mode

Take an accordion component, for instance. By default, we might not want to render every piece of content in all accordion sections, especially if there are lots of them, because it can slow down the initial loading time:

import React, { useState } from 'react'; function AccordionComponent() { const [activeSection, setActiveSection] = useState(null); return ( <div> <button onClick={() => setActiveSection(0)}>Section 1</button> <div role="region" hidden={activeSection !== 0}> <p>Content for Section 1</p> </div> <button onClick={() => setActiveSection(1)}>Section 2</button> <div role="region" hidden={activeSection !== 1}> <p>Content for Section 2</p> </div> </div> ); } export default AccordionComponent;

However, for accessibility, it might be better to always render all content but visually hide it instead:

import React, { useState } from 'react'; function AccessibleAccordionComponent() { const [activeSection, setActiveSection] = useState(null); return ( <div> <button aria-expanded={activeSection === 0} onClick={() => setActiveSection(activeSection === 0 ? null : 0)} > Section 1 </button> <div role="region" aria-hidden={activeSection !== 0} style={{ display: activeSection === 0 ? 'block' : 'none' }} > <p>Content for Section 1</p> </div> <button aria-expanded={activeSection === 1} onClick={() => setActiveSection(activeSection === 1 ? null : 1)} > Section 2 </button> <div role="region" aria-hidden={activeSection !== 1} style={{ display: activeSection === 1 ? 'block' : 'none' }} > <p>Content for Section 2</p> </div> </div> ); } export default AccessibleAccordionComponent;

When Not to Render All Content: Performance Mode

For applications that deal with a large amount of data or complex components, you might need to prioritize performance. Only render what’s necessary for the initial load and progressively load more as needed:

import React, { useState } from 'react'; function PerformanceAccordionComponent() { const [activeSection, setActiveSection] = useState(null); const [loadedSections, setLoadedSections] = useState({}); const handleSectionClick = (section) => { if (!loadedSections[section]) { setLoadedSections({ ...loadedSections, [section]: true, }); } setActiveSection(section === activeSection ? null : section); }; return ( <div> <button aria-expanded={activeSection === 0} onClick={() => handleSectionClick(0)} > Section 1 </button> {loadedSections[0] && ( <div role="region" aria-hidden={activeSection !== 0} style={{ display: activeSection === 0 ? 'block' : 'none' }} > <p>Content for Section 1</p> </div> )} <button aria-expanded={activeSection === 1} onClick={() => handleSectionClick(1)} > Section 2 </button> {loadedSections[1] && ( <div role="region" aria-hidden={activeSection !== 1} style={{ display: activeSection === 1 ? 'block' : 'none' }} > <p>Content for Section 2</p> </div> )} </div> ); } export default PerformanceAccordionComponent;

Here, we only load content when a section is clicked for the first time. This approach can help keep the initial rendering fast, especially useful for apps with many sections or heavy content.

Balancing the Act

So, which is more important: accessibility and SEO or performance? Well, it depends! For some projects, ensuring your app is accessible and SEO-friendly might be top priority, while for others, performance might take the lead.

If you're building an app with primarily logged-in users and you can control their environment, performance may be a bigger concern. But if your app needs to be accessible to a diverse audience and discovered through search engines, lean more towards accessibility and SEO.

Better yet, strive to strike a balance. Utilize server-side rendering where possible, enhance client-side dynamics progressively, and always consider your users' needs.

Accessibility, SEO, and performance are all crucial aspects of web development. While they might sometimes seem at odds with each other, they can coexist harmoniously with the right strategies in place. By understanding the costs and benefits of each, you can create more inclusive, discoverable, and performant content for all users. 🚀