Skip to main content

Overview

Griffo automatically preserves accessibility when splitting text. Screen readers receive the original, unsplit text while sighted users see the animated version.
Zero configuration required. Accessibility features are built-in and enabled by default.

Screen Reader Strategy

Griffo uses two strategies based on element type:

1. aria-label for Headings

Headings (<h1>-<h6>) and other landmark elements use aria-label:
Before
<h1>Hello World</h1>
After Split
<h1 aria-label="Hello World">
  <span aria-hidden="true">
    <span class="split-word">Hello</span>
    <span class="split-word">World</span>
  </span>
</h1>
Screen readers announce “Hello World” using the aria-label, ignoring the split spans.

2. sr-only Copy for Other Elements

Generic elements (<p>, <div>, <span>) get a hidden copy:
Before
<p>Animated paragraph</p>
After Split
<p>
  <span aria-hidden="true" data-griffo-visual="true">
    <span class="split-word">Animated</span>
    <span class="split-word">paragraph</span>
  </span>
  <span class="griffo-sr-only">Animated paragraph</span>
</p>

Screen Reader Styles

Griffo injects this CSS once per page:
.griffo-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border-width: 0;
}
From src/core/splitText.ts:407-425:
function injectSrOnlyStyles(): void {
  if (srOnlyStylesInjected || typeof document === 'undefined') return;

  const style = document.createElement('style');
  style.textContent = `
.griffo-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border-width: 0;
}`;
  document.head.appendChild(style);
  srOnlyStylesInjected = true;
}

Preserved Nested Elements

Griffo preserves semantic inline elements like <a>, <em>, <strong> with all attributes:
Before
<p>Read the <a href="/docs">documentation</a> for details.</p>
After Split
<p>
  <span aria-hidden="true" data-griffo-visual="true">
    <span class="split-word">Read</span>
    <span class="split-word">the</span>
    <a href="/docs">
      <span class="split-word">documentation</span>
    </a>
    <span class="split-word">for</span>
    <span class="split-word">details.</span>
  </span>
  <span class="griffo-sr-only">
    Read the <a href="/docs">documentation</a> for details.
  </span>
</p>
Screen reader users can navigate to the link and activate it normally.

Emphasis

Before
<p>This is <em>really</em> important.</p>
After Split
<p>
  <span aria-hidden="true" data-griffo-visual="true">
    <span class="split-word">This</span>
    <span class="split-word">is</span>
    <em>
      <span class="split-word">really</span>
    </em>
    <span class="split-word">important.</span>
  </span>
  <span class="griffo-sr-only">
    This is <em>really</em> important.
  </span>
</p>

Complex Nesting

Before
<h2>
  <a href="/blog/post-1">
    Read our <strong>latest</strong> blog post
  </a>
</h2>
After Split
<h2 aria-label="Read our latest blog post">
  <span aria-hidden="true" data-griffo-visual="true">
    <a href="/blog/post-1">
      <span class="split-word">Read</span>
      <span class="split-word">our</span>
      <strong>
        <span class="split-word">latest</span>
      </strong>
      <span class="split-word">blog</span>
      <span class="split-word">post</span>
    </a>
  </span>
</h2>
All inline elements are preserved in both the visual output and screen reader copy.

Allowed Inline Elements

Griffo preserves these inline elements:
<a>, <abbr>, <acronym>
From src/core/splitText.ts:393-398:
const INLINE_ELEMENTS = new Set([
  'a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'cite', 'code',
  'data', 'del', 'dfn', 'em', 'i', 'ins', 'kbd', 'mark', 'q', 's',
  'samp', 'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var',
]);

aria-label Allowed Elements

These elements support aria-label natively:
<h1>, <h2>, <h3>, <h4>, <h5>, <h6>
From src/core/splitText.ts:46-53:
const ARIA_LABEL_ALLOWED_TAGS = new Set([
  "h1", "h2", "h3", "h4", "h5", "h6",
  "a", "button", "img", "input", "select", "textarea",
  "table", "figure", "form", "fieldset", "dialog", "details",
  "section", "article", "nav", "aside", "header", "footer", "main",
]);

Testing with Screen Readers

macOS VoiceOver

  1. Enable: Cmd + F5
  2. Navigate: Control + Option + Arrow Keys
  3. Read all: Control + Option + A

NVDA (Windows)

  1. Download: nvaccess.org
  2. Navigate: Arrow Keys
  3. Read all: Insert + Down Arrow

JAWS (Windows)

  1. Navigate: Arrow Keys
  2. Read all: Insert + Down Arrow
  3. Virtual cursor: Num Pad Plus

Automatic Detection

Griffo automatically detects when to use each strategy:
// From splitText.ts:267-293
const trackAncestors = hasInlineDescendants(element);
const useAriaLabel =
  !trackAncestors &&
  ARIA_LABEL_ALLOWED_TAGS.has(element.tagName.toLowerCase());

performSplit(
  element,
  measuredWords,
  charClass,
  wordClass,
  lineClass,
  splitChars,
  splitWords,
  splitLines,
  {
    ariaHidden: useAriaLabel,
  }
);

if (trackAncestors) {
  // Has nested links/emphasis - use sr-only copy
  element.appendChild(createScreenReaderCopy(originalHTML));
} else if (useAriaLabel) {
  // Heading or landmark - use aria-label
  element.setAttribute("aria-label", text);
} else {
  // Generic element - use sr-only copy
  element.appendChild(createScreenReaderCopy(originalHTML));
}

Best Practices

Always use appropriate semantic elements (<h1>, <p>, <a>) instead of generic <div> or <span> tags.
Good
<SplitText>
  <h1>Page Title</h1>
</SplitText>
Avoid
<SplitText>
  <div className="heading">Page Title</div>
</SplitText>
Don’t rely solely on browser dev tools. Test with:
  • VoiceOver (macOS)
  • NVDA (Windows, free)
  • JAWS (Windows, commercial)
Ensure animated text doesn’t break keyboard navigation:
<SplitText>
  <nav>
    <a href="/home">Home</a>
    <a href="/about">About</a>
    <a href="/contact">Contact</a>
  </nav>
</SplitText>
Links remain focusable and maintain natural tab order.

Common Issues

Issue: aria-label Not Announced

Cause: Element doesn’t support aria-label Solution: Griffo automatically uses sr-only copy for these elements. No action needed. Cause: CSS transforms or z-index issues Solution: Ensure split spans don’t interfere with pointer events:
.split-word {
  pointer-events: none;
}

.split-word a {
  pointer-events: auto;
}

Issue: Screen Reader Reads Twice

Cause: Custom aria-label conflicts with Griffo’s label Solution: Remove custom aria-label before splitting:
<SplitText>
  <h1>{/* Griffo sets aria-label automatically */}
    Heading Text
  </h1>
</SplitText>

WCAG Compliance

Griffo helps meet these WCAG 2.1 success criteria:

1.3.1 Info and Relationships

Preserves semantic structure via aria-label and sr-only copies

2.1.1 Keyboard

Maintains keyboard focus order for nested interactive elements

2.4.4 Link Purpose

Preserves link context in both visual and screen reader output

4.1.2 Name, Role, Value

Maintains accessible names for all interactive elements

React

React component guide

Motion

Motion animations

Vanilla JS

Core API reference

Performance

Optimization tips

Build docs developers (and LLMs) love