Modern text wrapper library with TypeScript support, designed for seamless integration with GSAP and other animation libraries.
- TypeScript Support - Full type definitions included
- Single Bundle File - Just 13KB minified, one file to include
- 60%+ Performance Improvement - DocumentFragment batching, optimized DOM operations
- Zero Dependencies - Pure vanilla JavaScript
- Modular Architecture - 6 focused modules for maintainability
- Memory Safe - Built-in cleanup prevents memory leaks
- Cleaner API - Simplified, intuitive configuration
- Diacritical Support - Supports accented characters (ü, é, ñ, etc.)
- Accessibility Built-in - ARIA labels, aria-hidden, and title attributes for screen readers
- Character Groups - Smart selection system for targeting specific character subsets (NEW!)
- Animation Presets - Ready-to-use GSAP animations with one line of code (Optional GSAP feature)
- Text Transitions - Smoothly morph between different text content (Optional GSAP feature)
PS: Version 1.0 was never published.
npm install charwrapper// ES Module import
import CharWrapper from 'charwrapper';
const wrapper = new CharWrapper('.my-text', {
wrap: { chars: true }
});
const { chars } = wrapper.wrap();Download the latest release from GitHub:
- Visit: https://github.com/rowild/charwrapper/releases
- Download the
charwrapper.min.jsfile - Include it in your project
<script src="path/to/charwrapper.min.js"></script>Use directly from GitHub with native ES modules:
<script type="module">
import CharWrapper from 'https://raw.githubusercontent.com/rowild/charwrapper/main/dist/esm/CharWrapper.js';
const wrapper = new CharWrapper('.my-text', {
wrap: { chars: true }
});
const { chars } = wrapper.wrap();
</script>Note: For production environments, always use the CDN or NPM package for better performance and reliability. Direct GitHub usage is primarily for development and testing.
// webpack.config.js
module.exports = {
// ...
resolve: {
alias: {
'charwrapper': path.resolve(__dirname, 'node_modules/charwrapper/dist/esm/CharWrapper.js')
}
}
};// vite.config.js
export default {
// ...
resolve: {
alias: {
'charwrapper': 'charwrapper/dist/esm/CharWrapper.js'
}
}
};// rollup.config.js
export default {
// ...
plugins: [
resolve({
// Enables node_modules resolution
preferBuiltins: false
})
]
};<!DOCTYPE html>
<html>
<head>
<!-- GSAP from CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<!-- CharWrapper bundle -->
<script src="dist/charwrapper.min.js"></script>
</head>
<body>
<h1 class="my-text">Hello World</h1>
<script>
// Create wrapper instance
const wrapper = new CharWrapper('.my-text', {
wrap: { chars: true },
enumerate: { chars: true }
});
// Wrap the text
const { chars } = wrapper.wrap();
// Animate with GSAP
gsap.from(chars, {
opacity: 0,
y: 50,
stagger: 0.05,
duration: 0.8
});
// Clean up when done
wrapper.destroy();
</script>
</body>
</html>Note: CharWrapper uses a different API structure than GSAP SplitText (by design).
| Aspect | CharWrapper 2.0 | GSAP SplitText |
|---|---|---|
| Config Style | Nested/Grouped | Flat |
| Split Selection | wrap: { chars: true } |
type: 'chars,words,lines' |
| Enumeration | enumerate: { chars: true } |
charsClass: 'char++' |
| Class Names | classes: { char: 'x' } |
charsClass: 'x' |
| Philosophy | Organized, explicit | Concise, magic syntax |
Example comparison:
// CharWrapper 2.0 - Grouped & Explicit
new CharWrapper('.text', {
wrap: { chars: true, words: true },
enumerate: { chars: true },
classes: { char: 'c', word: 'w' }
});
// GSAP SplitText - Flat & Concise
new SplitText('.text', {
type: 'chars,words',
charsClass: 'c++',
wordsClass: 'w++'
});Both approaches are valid - CharWrapper prioritizes organization and discoverability, SplitText prioritizes brevity. See COMPARISON_WITH_GSAP_SPLITTEXT.md for detailed feature differences.
// One-liner for simple use cases
const wrapper = CharWrapper.create('.text', { wrap: { chars: true } });
const chars = wrapper.getChars();{
wrap: {
chars: true, // Wrap individual characters
words: false, // Wrap words (can combine with chars)
spaces: false, // Wrap space characters
specialChars: false // Wrap special characters (!?.,)
}
}{
enumerate: {
chars: true, // Add numbered classes (.char-001, .char-002)
words: false, // Add numbered classes to words
includeSpaces: false, // Include spaces in enumeration
includeSpecialChars: false // Include special chars in enumeration
}
}{
classes: {
char: 'char', // Base character class
word: 'word', // Base word class
space: 'char--space', // Space character class
special: 'char--special', // Special character class
regular: 'char--regular' // Regular character class
}
}{
tags: {
char: 'span', // Tag for character wrapping (span, div, i, em, strong, mark)
word: 'span' // Tag for word wrapping
}
}{
replaceSpaceWith: '\xa0', // Non-breaking space replacement
processing: {
stripHTML: true, // Remove HTML tags before processing
trimWhitespace: true, // Trim leading/trailing whitespace (preserved when adjacent to inline elements)
preserveStructure: true, // Maintain DOM structure
lazyWrap: false, // Wrap on-demand for performance
ordered: false // Order elements by data-custom-order attribute (for data attribute selection)
},
performance: {
useBatching: true, // Use DocumentFragment (recommended)
cacheSelectors: true // Cache DOM queries
},
accessibility: {
enabled: true, // Enable accessibility features
ariaLabel: 'auto', // 'auto' = use original text, 'none' = disabled, or custom string
ariaHidden: true, // Add aria-hidden="true" to wrapped elements
addTitle: true // Add title attribute if not present
}
}CharWrapper now includes built-in accessibility features to ensure screen reader compatibility:
const wrapper = new CharWrapper('.text', {
wrap: { chars: true },
accessibility: {
enabled: true, // Enable all accessibility features
ariaLabel: 'auto', // Adds aria-label with original text to root element
ariaHidden: true, // Adds aria-hidden="true" to all wrapped elements
addTitle: true // Adds title attribute if not present
}
});What this does:
<!-- Before wrapping -->
<div class="text">Hello World</div>
<!-- After wrapping (with accessibility enabled) -->
<div class="text" aria-label="Hello World" title="Hello World">
<span class="char" aria-hidden="true">H</span>
<span class="char" aria-hidden="true">e</span>
<span class="char" aria-hidden="true">l</span>
<span class="char" aria-hidden="true">l</span>
<span class="char" aria-hidden="true">o</span>
<span class="char" aria-hidden="true"> </span>
<span class="char" aria-hidden="true">W</span>
<span class="char" aria-hidden="true">o</span>
<span class="char" aria-hidden="true">r</span>
<span class="char" aria-hidden="true">l</span>
<span class="char" aria-hidden="true">d</span>
</div>Result: Screen readers read "Hello World" once (from aria-label) instead of "H. e. l. l. o. W. o. r. l. d."
Options:
ariaLabel: 'auto'- Uses original text content (default)ariaLabel: 'Custom text'- Uses your custom textariaLabel: 'none'- Disables aria-labelariaHidden: true- Hides wrapped elements from screen readers (default)addTitle: true- Adds title attribute for hover tooltips (default)
Note: Accessibility is enabled by default. This ensures your text animations are screen reader friendly out of the box!
Here's a comprehensive example showing ALL available configuration options for reference:
const wrapper = new CharWrapper('.text', {
// Wrap Options - What to wrap
wrap: {
chars: true, // Wrap individual characters
words: false, // Wrap words (can combine with chars)
spaces: false, // Wrap space characters
specialChars: false // Wrap special characters (!?.,)
},
// Enumeration Options - Add numbered classes
enumerate: {
chars: false, // Add numbered classes (.char-001, .char-002)
words: false, // Add numbered classes to words
includeSpaces: false, // Include spaces in enumeration count
includeSpecialChars: false // Include special chars in enumeration count
},
// CSS Classes - Customize the class names used
classes: {
char: 'char', // Base character class
word: 'word', // Base word class
space: 'char--space', // Space character class
special: 'char--special', // Special character class
regular: 'char--regular' // Regular character class
},
// HTML Tags - Choose the element type for wrapping
tags: {
char: 'span', // Tag for character wrapping (span, div, i, em, strong, mark)
word: 'span' // Tag for word wrapping
},
// Data Attributes - Customize data attribute names (for data-driven selection)
dataAttributes: {
subSetName: 'subSetName', // data-sub-set-name
subSetClass: 'subSetCharsClass', // data-sub-set-chars-class
customOrder: 'customOrder' // data-custom-order
},
// Advanced Options
replaceSpaceWith: '\\xa0', // Replace spaces with non-breaking space
// Processing Options - Text processing behavior
processing: {
stripHTML: true, // Remove HTML tags before processing
trimWhitespace: true, // Trim leading/trailing whitespace (preserved when adjacent to inline elements)
preserveStructure: true, // Maintain DOM structure
lazyWrap: false, // Wrap on-demand for performance
ordered: false // Order elements by data-custom-order attribute (for data attribute selection)
},
// Performance Options
performance: {
useBatching: true, // Use DocumentFragment for DOM updates (recommended)
cacheSelectors: true // Cache DOM queries
},
// Accessibility Options
accessibility: {
enabled: true, // Enable accessibility features
ariaLabel: 'auto', // 'auto' = use original text, 'none' = disabled, or custom string
ariaHidden: true, // Add aria-hidden="true" to wrapped elements
addTitle: true // Add title attribute if not present
},
// Character Groups - Smart selection system for character subsets
groups: {
// Examples of different group types (these are optional):
vowels: /[aeiou]/i, // Pattern matching
everyThird: { nth: 3 }, // Every Nth character
firstThree: { indices: [0, 1, 2] }, // Specific indices
// Custom filter function
firstLetters: {
custom: (char, index, context) => context.isFirstInWord,
class: 'first-letter'
}
}
});CharWrapper 2.0 introduces Character Groups - a powerful feature for selecting and animating specific character subsets. This is something GSAP SplitText doesn't offer!
Character groups allow you to organize wrapped characters into named collections based on patterns, positions, or custom logic. You can then animate each group independently.
const wrapper = new CharWrapper('.text', {
wrap: { chars: true },
groups: {
vowels: /[aeiou]/i,
consonants: /[bcdfghjklmnpqrstvwxyz]/i,
everyThird: { nth: 3 }
}
});
const { chars, groups } = wrapper.wrap();
// Animate vowels and consonants separately
gsap.from(groups.vowels, { opacity: 0, color: '#ff6b9d', stagger: 0.05 });
gsap.from(groups.consonants, { y: 20, stagger: 0.03, delay: 0.3 });CharWrapper includes predefined patterns you can use instantly:
import { PREDEFINED_GROUPS } from 'charwrapper';
// Basic character types
PREDEFINED_GROUPS.vowels // a, e, i, o, u (case insensitive)
PREDEFINED_GROUPS.consonants // All consonants
PREDEFINED_GROUPS.numbers // 0-9
PREDEFINED_GROUPS.lowercase // a-z
PREDEFINED_GROUPS.uppercase // A-Z
// Punctuation
PREDEFINED_GROUPS.punctuation // . , ! ? ; :
PREDEFINED_GROUPS.quotes // " ' ` ´
PREDEFINED_GROUPS.brackets // [ ] ( ) { }
// Diacritics (accented characters) - Perfect for multilingual content!
PREDEFINED_GROUPS.diacritics // All accented characters (à, é, ü, ñ, etc.)
// Language-specific diacritics
PREDEFINED_GROUPS.french // é, è, ç, à, û, etc.
PREDEFINED_GROUPS.german // ä, ö, ü, ß
PREDEFINED_GROUPS.spanish // á, é, í, ó, ú, ñ, ¿, ¡
PREDEFINED_GROUPS.portuguese // ã, õ, ç
PREDEFINED_GROUPS.slavic // Czech, Polish, Croatian characters
PREDEFINED_GROUPS.scandinavian // å, æ, ø
// Special symbols
PREDEFINED_GROUPS.currency // $, €, £, ¥, ₹, ₽
PREDEFINED_GROUPS.math // +, -, =, ×, ÷, ±, ∞, ≈
PREDEFINED_GROUPS.emoji // Emoji ranges{
groups: {
vowels: /[aeiou]/i,
specialChars: /[!@#$%^&*]/
}
}{
groups: {
everySecond: { nth: 2 }, // Every 2nd character
everyThird: { nth: 3 }, // Every 3rd character
everyFifth: { nth: 5 } // Every 5th character
}
}{
groups: {
firstThree: { indices: [0, 1, 2] },
highlights: { indices: [5, 10, 15, 20] }
}
}{
groups: {
keywords: {
words: ['CharWrapper', 'animation', 'GSAP'],
class: 'keyword-highlight' // Optional: add CSS class
}
}
}The most powerful option - full control with context awareness:
{
groups: {
firstLetters: {
custom: (char, index, context) => context.isFirstInWord,
class: 'first-letter'
},
lastLetters: {
custom: (char, index, context) => context.isLastInWord
},
oddPositions: {
custom: (char, index) => index % 2 === 1
}
}
}CharContext API:
char(string) - The characterindex(number) - Character position in textisFirstInWord(boolean) - Is first character of a wordisLastInWord(boolean) - Is last character of a wordwordIndex(number) - Which word this character belongs to
// Perfect for emphasizing accented characters in French text
const wrapper = new CharWrapper('.french-text', {
wrap: { chars: true },
groups: {
accents: /[àâæçéèêëïîôùûüÿœ]/i
}
});
const { groups } = wrapper.wrap();
gsap.to(groups.accents, {
color: '#ff6b9d',
scale: 1.2,
stagger: 0.1,
yoyo: true,
repeat: -1,
repeatDelay: 2
});const wrapper = new CharWrapper('.price', {
wrap: { chars: true },
groups: {
numbers: /[0-9]/,
currency: /[$€£¥]/
}
});
const { groups } = wrapper.wrap();
// Animate numbers and currency symbols differently
gsap.from(groups.currency, { scale: 0, duration: 0.5 });
gsap.from(groups.numbers, {
opacity: 0,
y: -20,
stagger: 0.05,
delay: 0.3
});const wrapper = new CharWrapper('.headline', {
wrap: { chars: true },
groups: {
firstLetters: {
custom: (char, index, context) => context.isFirstInWord,
class: 'drop-cap'
}
}
});
const { groups } = wrapper.wrap();
gsap.from(groups.firstLetters, {
scale: 2,
color: '#ffd700',
stagger: 0.15,
ease: 'back.out(1.7)'
});const wrapper = new CharWrapper('.text', {
wrap: { chars: true },
groups: {
vowels: /[aeiou]/i,
numbers: /[0-9]/,
everyThird: { nth: 3 },
firstLetters: {
custom: (char, index, context) => context.isFirstInWord
}
}
});
const { groups } = wrapper.wrap();
// Animate each group with different effects
gsap.from(groups.vowels, { opacity: 0, stagger: 0.02 });
gsap.from(groups.numbers, { scale: 2, stagger: 0.1 });
gsap.from(groups.everyThird, { color: '#ff6b9d' });
gsap.from(groups.firstLetters, { y: -30, ease: 'bounce.out' });Check out examples/05-character-groups.html for a complete interactive demonstration with:
- Vowels vs consonants separation
- Multilingual diacritics (French, German, Spanish)
- Mixed content separation (letters, numbers, punctuation)
- Custom filters (first/last letters of words)
- Nth character patterns
- Word-based selection
Note: Character groups are completely optional. If you don't configure any groups, the groups object in the result will simply be empty {}.
CharWrapper includes a powerful data attribute selection system that allows you to define text segments in HTML using data attributes instead of CSS selectors. This is perfect for:
- Dynamic content - CMS-driven text animations
- Structured data - Business cards, profiles, forms
- Custom ordering - Animate elements in any sequence regardless of HTML order
- Mixing content types - Combine text, graphics, and interactive elements
Instead of wrapping elements individually, wrap a container and use data attributes to organize and control the content:
// Wrap the entire container - all text inside will be wrapped
const wrapper = new CharWrapper('.profile-card', {
wrap: { chars: true }
});
// All text nodes inside .profile-card are now wrapped
const { chars } = wrapper.wrap();
// Animate all characters in document order
gsap.from(chars, { opacity: 0, stagger: 0.02 });The data-sub-set-name attributes provide semantic structure and enable features like custom classes and exclusion.
<div class="profile">
<h1 data-sub-set-name="first_name">John</h1>
<h1 data-sub-set-name="last_name">Van der Slice</h1>
<!-- Mix in non-text elements -->
<div data-sub-set-name="divider_line" class="divider"></div>
<p data-sub-set-name="profession_1">composer</p>
<p data-sub-set-name="profession_2">teacher</p>
<p data-sub-set-name="profession_3">analyst</p>
</div>IMPORTANT: CharWrapper wraps the container element and processes all text nodes inside it in HTML document order. The order elements appear in your HTML determines their animation sequence:
// Wrap the container - all text inside will be wrapped in document order
const wrapper = new CharWrapper('.profile', {
wrap: { chars: true }
});
const { chars } = wrapper.wrap();
// Animate all characters in the order they appear in HTML
gsap.from(chars, { opacity: 0, stagger: 0.02 });The text is wrapped in HTML document order - the order elements appear in your HTML source. If you want to change the animation order, you can either rearrange the HTML elements or use the ordered: true processing option:
const wrapper = new CharWrapper('.profile', {
wrap: { chars: true },
processing: { ordered: true } // This will order elements by data-custom-order attribute
});When ordered: true, elements are sorted by their data-custom-order attribute values instead of HTML document order.
<div class="profile">
<!-- First name animates first -->
<h1 data-sub-set-name="first_name">John</h1>
<!-- Last name animates second -->
<h1 data-sub-set-name="last_name">Van der Slice</h1>
<!-- Professions animate in the order they appear -->
<p data-sub-set-name="profession_1">composer</p>
<p data-sub-set-name="profession_2">teacher</p>
<p data-sub-set-name="profession_3">analyst</p>
</div>To control specific element animations separately, target them with CSS selectors after the character animation:
const wrapper = new CharWrapper('.profile', { wrap: { chars: true } });
const { chars } = wrapper.wrap();
const tl = gsap.timeline();
// First animate all text
tl.from(chars, { opacity: 0, stagger: 0.02 });
// Then animate specific elements (e.g., a divider)
tl.from('.divider', { scaleX: 0 }, '-=0.5');Add element-specific classes using data-sub-set-chars-class:
<div class="profile">
<!-- Add 'name-char' class to all characters in this element -->
<h1 data-sub-set-name="first_name"
data-sub-set-chars-class="name-char">John</h1>
<!-- Add 'profession-char' class to all characters in this element -->
<p data-sub-set-name="profession_1"
data-sub-set-chars-class="profession-char">composer</p>
</div>/* Target characters in specific elements */
.name-char {
color: #ff6b9d;
font-weight: bold;
}
.profession-char {
color: #4ecdc4;
font-style: italic;
}Exclude elements from wrapping using data-sub-set-name="_exclude_":
<div class="profile">
<h1 data-sub-set-name="name">John Doe</h1>
<!-- This will be skipped during wrapping -->
<div data-sub-set-name="_exclude_">
<span>This text will NOT be wrapped</span>
</div>
<p data-sub-set-name="profession">composer</p>
</div>Key Feature: Data attributes work with any element, not just text! This lets you combine text animations with graphic elements:
<div class="business-card">
<h1 data-sub-set-name="first_name">John</h1>
<h1 data-sub-set-name="last_name">Van der Slice</h1>
<!-- Animate a divider line -->
<div data-sub-set-name="divider_line" class="divider"></div>
<p data-sub-set-name="title">Lead Composer</p>
</div>const wrapper = new CharWrapper('[data-sub-set-name]', {
wrap: { chars: true }
});
const { chars } = wrapper.wrap();
// Create timeline
const tl = gsap.timeline();
// First name and last name appear
tl.from(chars, { opacity: 0, y: 20, stagger: 0.02 });
// Then animate the divider
tl.from('.divider', {
scaleX: 0,
transformOrigin: 'center',
duration: 0.6,
ease: 'power2.out'
}, '-=0.3');
// Finally the title
tl.from('.title', { opacity: 0, y: 10 }, '-=0.2');You can customize data attribute names:
const wrapper = new CharWrapper('[data-profile-item]', {
wrap: { chars: true },
dataAttributes: {
subSetName: 'profileItem', // data-profile-item
subSetClass: 'profileClass', // data-profile-class
customOrder: 'sequence' // data-sequence
}
});<!DOCTYPE html>
<html>
<head>
<style>
.profile-card {
background: white;
padding: 2rem;
border-radius: 10px;
text-align: center;
}
.divider {
height: 2px;
background: linear-gradient(to right, transparent, #333, transparent);
margin: 1rem 0;
transform-origin: center;
}
.char { display: inline-block; }
</style>
</head>
<body>
<div class="profile-card">
<h1 data-sub-set-name="first_name">John</h1>
<h1 data-sub-set-name="last_name">Van der Slice</h1>
<div data-sub-set-name="divider_line" class="divider"></div>
<p data-sub-set-name="profession_1">composer</p>
<p data-sub-set-name="profession_2">teacher</p>
<p data-sub-set-name="profession_3">analyst</p>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="charwrapper.min.js"></script>
<script>
// Wrap the entire profile card
const wrapper = new CharWrapper('.profile-card', {
wrap: { chars: true }
});
const { chars } = wrapper.wrap();
const tl = gsap.timeline();
// Animate all text characters in document order
tl.from(chars, {
opacity: 0,
y: 20,
stagger: 0.02,
ease: 'back.out(1.7)'
});
// Animate divider from center outward
tl.from('.divider', {
scaleX: 0,
duration: 0.6,
ease: 'power2.out'
}, '-=0.5');
</script>
</body>
</html>Check out examples/gsap/09-data-attributes.html, examples/animejs/09-data-attributes.html, and examples/waapi/09-data-attributes.html for complete interactive demonstrations showing:
- Data-driven element selection
- HTML document order vs custom ordering
- Mixing text and graphic elements
- Custom class assignment per element
- Excluding elements from processing
Note: The data attributes feature is completely optional. Most users will use CSS selectors ('.text', '#heading') and won't need data attributes unless building dynamic, data-driven animations.
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="charwrapper.min.js"></script>Instead of writing custom GSAP code every time, use built-in presets with a single method call:
const wrapper = new CharWrapper('.text', { wrap: { chars: true } });
wrapper.wrap();
// Use presets instead of writing GSAP code:
wrapper.animate('fadeInStagger');
wrapper.animate('typewriter', { stagger: 0.05 });
wrapper.animate('wave', { amplitude: 30 });Entrance Animations:
fadeInStagger- Classic fade with staggerslideInUp,slideInDown,slideInLeft,slideInRight- Directional slidesscaleIn- Pop in from centerrotateIn- Spinning entranceelasticBounce- Bouncy entrancetypewriter- Classic typing effectwave- Wave-like stagger patternglitch- Digital glitch effect
Loop Animations:
floatingWave- Continuous floating wavepulse- Breathing effectcolorCycle- Color transitionsshimmer- Shine/shimmer effect
Exit Animations:
fadeOut,slideOutDown,scaleOut- Standard exitsexplode- Characters scatter in random directions
Interactive:
hoverBounce- Auto-attach hover listenersclickSpin- Auto-attach click listeners
All presets accept custom options:
wrapper.animate('fadeInStagger', {
duration: 1,
stagger: 0.05,
ease: 'power2.out',
delay: 0.5,
groups: 'vowels' // Animate only specific groups!
});Register your own reusable presets:
CharWrapper.registerPreset('myEffect', (elements, options) => {
return gsap.from(elements, {
opacity: 0,
scale: 2,
rotation: 360,
stagger: options.stagger || 0.05
});
});
wrapper.animate('myEffect');✅ Faster development - Common effects in one line ✅ Beginner-friendly - No GSAP knowledge required ✅ Still flexible - Customize any preset ✅ Works with groups - Combine with character groups ✅ Returns GSAP timeline - Advanced users can manipulate it
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="charwrapper.min.js"></script>Smoothly morph from one text to another with intelligent character matching:
const wrapper = new CharWrapper('.text', { wrap: { chars: true } });
wrapper.wrap();
// Transition to new text
wrapper.transitionTo('New Text Here');
// With options
wrapper.transitionTo('Updated!', {
strategy: 'smart',
addDuration: 0.5,
removeDuration: 0.3,
stagger: 0.02
});1. Smart (default) - Intelligently matches characters between old and new text:
wrapper.transitionTo('New Text', { strategy: 'smart' });- Reuses matching characters
- Only animates what changed
- Best for similar text
2. Sequential - Removes all, then adds all:
wrapper.transitionTo('Completely Different', { strategy: 'sequential' });- Clean and simple
- Best for very different text
wrapper.transitionTo('New Text', {
strategy: 'smart', // 'smart' or 'sequential'
addDuration: 0.4, // Duration for adding characters
removeDuration: 0.4, // Duration for removing characters
stagger: 0.02, // Stagger between characters
ease: 'power2.out', // GSAP easing
onComplete: () => { // Callback when done
console.log('Transition complete!');
}
});Counter:
let count = 0;
function increment() {
count++;
wrapper.transitionTo(String(count));
}Status Messages:
wrapper.transitionTo('Loading...');
// later
wrapper.transitionTo('Success!');Chained Transitions:
wrapper.transitionTo('First', {
onComplete: () => {
setTimeout(() => {
wrapper.transitionTo('Second', {
onComplete: () => {
wrapper.transitionTo('Done!');
}
});
}, 1000);
}
});✅ Smooth morphing - No jarring content changes ✅ Intelligent matching - Reuses characters when possible ✅ Perfect for dynamic content - Counters, status updates, live data ✅ Returns GSAP timeline - Full control for advanced users ✅ Auto-updates groups - Character groups are re-evaluated after transition
Open examples/index.html in your browser to see all examples:
Staggered entrance effects with fade, slide, scale, and wave animations.
const wrapper = new CharWrapper('.text', {
wrap: { chars: true },
enumerate: { chars: true }
});
const { chars } = wrapper.wrap();
gsap.from(chars, {
opacity: 0,
y: 50,
stagger: 0.03,
ease: 'back.out(1.7)'
});Matrix-style decoding, character randomization, glitch effects.
// Matrix decode effect
const originalText = chars.map(el => el.textContent);
chars.forEach((char, i) => {
gsap.to(char, {
duration: 0.05,
repeat: 20,
onRepeat: () => char.textContent = getRandomChar(),
onComplete: () => char.textContent = originalText[i]
});
});Mouse-reactive animations with bounce, magnetic pull, and ripple effects.
chars.forEach(char => {
char.addEventListener('mouseenter', () => {
gsap.to(char, { y: -20, duration: 0.3 });
gsap.to(char, { y: 0, duration: 0.5, delay: 0.3, ease: 'bounce.out' });
});
});GSAP ScrollTrigger integration with parallax and progressive blur.
gsap.registerPlugin(ScrollTrigger);
gsap.from(chars, {
opacity: 0,
y: 30,
stagger: 0.02,
scrollTrigger: {
trigger: '.text',
start: 'top 80%',
end: 'top 50%',
scrub: 1
}
});new CharWrapper(target, config)Parameters:
target(string|Element) - CSS selector or DOM elementconfig(Object) - Configuration options
Wraps the text content.
- Returns:
{ chars: Array<HTMLElement>, words: Array<HTMLElement>, groups: GroupResult }chars- Array of all wrapped character elementswords- Array of all wrapped word elementsgroups- Object containing grouped character elements (e.g.,{ vowels: [...], consonants: [...] })
Restores original content.
Unwraps and wraps again (useful for re-animation).
Cleans up and removes all references (prevents memory leaks).
Returns array of wrapped character elements.
Returns array of wrapped word elements.
Returns specific character element by index.
Returns specific word element by index.
Filters characters by type ('regular', 'space', 'special').
const regularChars = wrapper.getCharsByType('regular');
const spaces = wrapper.getCharsByType('space');Returns characters matching a specific class.
Checks if element is currently wrapped.
Returns the root DOM element.
Returns current configuration (read-only copy).
Returns instance metadata (id, charCount, wordCount, etc.).
Animate characters using a preset animation.
wrapper.animate('fadeInStagger');
wrapper.animate('wave', { amplitude: 30, duration: 1 });
wrapper.animate('typewriter', { stagger: 0.05, groups: 'vowels' });Parameters:
presetName(string) - Name of the animation presetoptions(PresetOptions) - Animation options (duration, stagger, delay, ease, groups, etc.)
Returns: GSAP timeline or tween, or null if preset not found
Note: Requires GSAP to be loaded. Returns null if element is not wrapped.
Transition to new text content with smooth animation.
wrapper.transitionTo('New Text Here');
wrapper.transitionTo('Updated!', {
strategy: 'smart',
addDuration: 0.5,
removeDuration: 0.3,
stagger: 0.02
});Parameters:
newText(string) - The new text to transition tooptions(TransitionOptions) - Transition options
Options:
strategy- 'smart' (default) or 'sequential'addDuration- Duration for adding characters (default: 0.4)removeDuration- Duration for removing characters (default: 0.4)stagger- Stagger between characters (default: 0.02)ease- GSAP easing (default: 'power2.out')onComplete- Callback function
Returns: GSAP timeline or null
Note: Requires GSAP to be loaded. Automatically updates character groups after transition.
Creates and wraps in one call.
const wrapper = CharWrapper.create('.text', { wrap: { chars: true } });Wraps multiple elements at once.
const wrappers = CharWrapper.wrapMultiple(['.text1', '.text2'], {
wrap: { chars: true }
});Register a custom animation preset.
CharWrapper.registerPreset('myEffect', (elements, options) => {
return gsap.from(elements, {
opacity: 0,
scale: 2,
rotation: 360,
stagger: options.stagger || 0.05
});
});
// Use it
wrapper.animate('myEffect');Parameters:
name(string) - Preset namefn(function) - Preset function that receives (elements, options) and returns a GSAP timeline/tween
CharWrapper/
├── dist/ # Built bundles
│ ├── charwrapper.js # Browser bundle (IIFE format, 36KB)
│ ├── charwrapper.min.js # Minified browser bundle (IIFE format, 13KB) ← Use this for browsers!
│ ├── charwrapper.cjs.js # Node.js bundle (CommonJS format, 52KB)
│ ├── charwrapper.cjs.min.js # Minified Node.js bundle (CommonJS format, 21KB)
│ └── esm/ # ES modules (for npm/bundlers)
│ ├── CharWrapper.js
│ ├── CharWrapper.d.ts # TypeScript definitions
│ ├── config.js
│ ├── utils.js
│ └── ...
├── src/ # TypeScript source files
│ ├── CharWrapper.ts # Main class
│ ├── config.ts # Configuration & types
│ ├── utils.ts # Utilities
│ ├── WrapperFactory.ts # Element factory
│ ├── DOMProcessor.ts # DOM operations
│ └── SelectionStrategy.ts # Selection patterns
├── build-bundle.js # Esbuild script for browser bundles
├── package.json # NPM package configuration
├── tsconfig.json # TypeScript configuration
├── README.md # Comprehensive documentation
├── QUICKSTART.md # Quick start guide
└── COMPARISON_WITH_GSAP_SPLITTEXT.md # API comparison with GSAP SplitText
npm install # Install dependencies
npm run build # Compile TypeScript to esm/
npm run bundle # Build + create browser bundles
npm run watch # Watch mode for development
npm run clean # Remove all build outputFull TypeScript support with type definitions:
import CharWrapper from 'charwrapper';
const wrapper = new CharWrapper('.text', {
wrap: { chars: true },
enumerate: { chars: true }
});
// Full type inference and autocomplete
const { chars, words } = wrapper.wrap();- DocumentFragment Batching - Reduces reflows from 100+ to 1
- WeakMap Caching - Prevents memory leaks, auto garbage collection
- Single DOM Clear - Removed redundant
innerHTML/innerTextoperations - Native Array Methods - Replaced lodash with native
sort() - Query Caching - Stores selector results when enabled
| Metric | v1.0 (Old) | v2.0 (New) | Improvement |
|---|---|---|---|
| DOM Reflows | 100+ | 1-2 | 98% reduction |
| Dependencies | lodash | none | 100% reduction |
| Memory Leaks | Yes | No | Fixed |
| Load Time | ~150ms | ~50ms | 66% faster |
- Hero Text Animations - Stunning entrance effects
- Interactive Typography - Mouse-reactive text
- Loading Screens - Scramble/decode effects
- Scroll Narratives - Story-driven scroll animations
- UI Microinteractions - Button and link hover effects
- Data Visualization - Animated number counters
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Opera 76+
Uses modern ES6+ features (private fields, optional chaining, nullish coalescing).
| File | Size | Use Case |
|---|---|---|
charwrapper.min.js |
13KB | Production (recommended) |
charwrapper.js |
36KB | Development/debugging |
| ESM modules | ~40KB | NPM package (tree-shakeable) |
let wrapper = new CharWrapper({
rootSetIdentifier: '.text',
wrapChars: true,
enumerateRootSet: { includeSpaces: true },
characterWrapTag: 'span',
// ...30+ options
});
wrapper.initializeWrap();<script src="dist/charwrapper.min.js"></script>
<script>
const wrapper = new CharWrapper('.text', {
wrap: { chars: true },
enumerate: { chars: true, includeSpaces: true },
tags: { char: 'span' }
});
const { chars, words } = wrapper.wrap();
wrapper.destroy(); // Don't forget cleanup!
</script>- Nested HTML tags are stripped before processing (intentional for clean output)
<br>tags are removed during text processing- Inline styles on original text are not preserved
This is a personal project, but suggestions are welcome! Please open an issue to discuss improvements.
MIT License - Free to use in personal and commercial projects.
- Always call
destroy()when removing elements (especially in SPAs) - Use
useBatching: truefor best performance (default) - Combine with GSAP's
staggerfor beautiful effects - Filter by character type to animate only specific characters
- Use
rewrap()instead of creating new instances
Check out the examples folder for production-ready code:
- All 4 examples are fully commented
- Copy-paste ready
- Best practices demonstrated
- Performance optimized
Built with ❤️ for modern web animations
CharWrapper 2.0 - Zero dependencies, maximum performance
GSAP SplitText is the professional, feature-rich industry standard with 14+ advanced features. CharWrapper 2.0 is a lighter, independent alternative focused on character/word wrapping basics.
Both libraries provide:
| Feature | CharWrapper 2.0 | GSAP SplitText |
|---|---|---|
| Character splitting | ✅ wrap: { chars: true } |
✅ type: "chars" |
| Word splitting | ✅ wrap: { words: true } |
✅ type: "words" |
| Nested wrapping | ✅ Words contain chars | ✅ Words contain chars |
| Custom CSS classes | ✅ classes: { char: 'x' } |
✅ charsClass: 'x' |
| Class enumeration | ✅ enumerate: { chars: true } |
✅ charsClass: 'char++' |
| Custom HTML tags | ✅ tags: { char: 'span' } |
✅ tag: 'span' |
| Destroy/cleanup | ✅ wrapper.destroy() |
✅ splitText.revert() |
| Re-wrapping | ✅ wrapper.rewrap() |
✅ splitText.split(newVars) |
- Both use ES6 classes
- Both return element arrays for GSAP animation
- Both support method chaining
- Both designed specifically for GSAP integration
| Feature | CharWrapper 2.0 | GSAP SplitText |
|---|---|---|
| Split by lines | ❌ NOT SUPPORTED | ✅ type: "lines" |
| Line detection | ❌ N/A | ✅ Intelligent algorithm |
| Line reflow handling | ❌ N/A | ✅ autoSplit: true |
| Deep slicing | ❌ N/A | ✅ Handles nested elements across lines |
Impact: This is the biggest missing feature in CharWrapper. Line splitting is crucial for many professional text animations.
| Feature | CharWrapper 2.0 | GSAP SplitText |
|---|---|---|
| aria-label | ❌ Not implemented | ✅ Auto-added to parent |
| aria-hidden | ❌ Not implemented | ✅ Auto-added to split elements |
| Accessibility modes | ❌ None | ✅ aria: "auto"|"hidden"|"none" |
| Screen reader friendly | ✅ Yes | ✅ Yes |
Impact: CharWrapper now includes accessibility features for screen readers.
| Feature | CharWrapper 2.0 | GSAP SplitText |
|---|---|---|
| Mask property | ❌ Not supported | ✅ mask: "lines"|"words"|"chars" |
| Automatic masking | ❌ Manual CSS needed | ✅ Creates wrapper with overflow: hidden |
| Reveal animations | ✅ Built-in, easy |
| Feature | CharWrapper 2.0 | GSAP SplitText |
|---|---|---|
| Font loading detection | ❌ Not supported | ✅ autoSplit: true + font observer |
| Resize observer | ❌ Not supported | ✅ Auto re-splits on resize |
| Debounced re-splitting | ❌ N/A | ✅ 200ms debounce |
| Responsive text | ✅ Automatic |
| Feature | CharWrapper 2.0 | GSAP SplitText |
|---|---|---|
| White space reduction | ✅ trimWhitespace: true |
✅ reduceWhiteSpace: true |
Preserve <pre> formatting |
❌ No | ✅ Honors extra spaces + auto <br> |
| Custom word delimiter | ❌ Only space | ✅ wordDelimiter: /regex/ or custom |
| Ignore elements | ✅ Via _exclude_ data attr |
✅ ignore: ".keep-whole" |
| Smart wrap | ❌ No | ✅ Prevents odd breaks |
| Deep slice | ❌ No | ✅ Subdivides nested <strong> across lines |
| Feature | CharWrapper 2.0 | GSAP SplitText |
|---|---|---|
| Character grouping | ✅ Advanced grouping by pattern, position, custom functions | ❌ Not Supported |
| Pattern matching | ✅ Regex-based grouping | ❌ Not Supported |
| nth character grouping | ✅ Every Nth character grouping | ❌ Not Supported |
| Custom filter functions | ✅ Full context-aware filters | ❌ Not Supported |
| Predefined patterns | ✅ Language-specific diacritics, punctuation groups, etc. | ❌ Not Supported |
Impact: CharWrapper's character grouping is a unique feature not available in GSAP SplitText.
| Feature | CharWrapper 2.0 | GSAP SplitText |
|---|---|---|
| File size | ~13KB minified (IIFE) | ~14KB (50% smaller after rewrite!) |
| TypeScript | ✅ JSDoc with TypeScript compatibility | ✅ Written in TypeScript |
| Bundle optimization | ✅ Multiple formats (IIFE, CJS, ESM) | ✅ Tree-shakeable |
| Performance monitoring | ❌ No | ✅ Internal optimizations |
| DocumentFragment batching | ✅ Reduces DOM reflows significantly | ✅ Optimized |
// CharWrapper 2.0 - Grouped options
new CharWrapper('.text', {
wrap: { chars: true, words: true },
enumerate: { chars: true, includeSpaces: true },
classes: { char: 'c', word: 'w' },
tags: { char: 'span' }
});
// GSAP SplitText - Flat options
new SplitText('.text', {
type: 'chars,words',
charsClass: 'c++',
wordsClass: 'w++',
tag: 'span'
});Winner: SplitText is more concise. CharWrapper's grouped approach is more organized but verbose.
// CharWrapper 2.0 - Separate config
enumerate: { chars: true }
// Result: .char .char-001 .char-002
// GSAP SplitText - In class name
charsClass: 'char++'
// Result: .char .char1 .char2Winner: SplitText's ++ syntax is more elegant.
| Feature | CharWrapper 2.0 | GSAP SplitText |
|---|---|---|
| CSS variable indices | ❌ No | ✅ propIndex: true → --char: 3 |
| Custom text preparation | ❌ No | ✅ prepareText: fn callback |
| onSplit callback | ❌ No | ✅ onSplit: fn with auto-timing |
| onRevert callback | ❌ No | ✅ onRevert: fn |
| Special char handling | ✅ Advanced via character groups | ✅ specialChars: /regex/ or array |
| Mask arrays | ❌ No | ✅ Separate masks property |
| Animation presets | ✅ Built-in GSAP animations | ❌ Not Supported |
| Text transitions | ✅ Morph between different text content | ❌ Not Supported |
| Data attribute selection | ✅ Structure-driven content organization | ❌ Not Supported |
Why it matters: Line splitting is essential for:
- Title reveals that animate line-by-line
- Paragraph entrance animations
- Staggered line effects
- Responsive text that reflows properly
Example use case:
// SplitText can do this:
const split = new SplitText('.title', { type: 'lines' });
gsap.from(split.lines, {
y: 100,
opacity: 0,
stagger: 0.2
});
// CharWrapper cannot! ❌Impact Level: 🔴 CRITICAL - This is the #1 feature professionals expect.
Why it matters:
- Screen readers can read split text with aria-label
- Legal requirement in many countries (ADA, WCAG 2.1)
- CharWrapper now supports accessibility
Current state:
<!-- CharWrapper output (now accessible) -->
<div class="text" aria-label="Hi">
<span class="char" aria-hidden="true">H</span>
<span class="char" aria-hidden="true">i</span>
</div>
<!-- Screen reader reads: "Hi" (correct) ✅ -->
<!-- SplitText output (accessible) -->
<div class="text" aria-label="Hi">
<span class="char" aria-hidden="true">H</span>
<span class="char" aria-hidden="true">i</span>
</div>
<!-- Screen reader reads: "Hi" (correct) ✅ -->Impact Level: 🟢 RESOLVED - Now supports accessibility features.
Why it matters:
- Clean text reveals without clipping issues
- Professional-looking line-by-line animations
- Proper overflow handling
Example:
// SplitText
const split = new SplitText('.text', {
type: 'lines',
mask: 'lines' // ✅ Auto-creates masks
});
gsap.from(split.masks, { scaleY: 0, transformOrigin: 'top' });
// CharWrapper
// ❌ Must manually create wrapper elements and CSSImpact Level: 🟡 HIGH - Very common in professional work.
Why it matters:
- Fonts loading late breaks layout
- Window resizing breaks line splits
- Manual rewrapping is tedious
SplitText solution:
const split = new SplitText('.text', {
type: 'lines',
autoSplit: true // ✅ Auto re-splits on font load & resize
});CharWrapper workaround:
// ❌ Must manually detect and rewrap
window.addEventListener('resize', debounce(() => {
wrapper.rewrap();
}, 200));
document.fonts.ready.then(() => {
wrapper.rewrap();
});Impact Level: 🟡 HIGH - Essential for responsive sites.
Why it matters:
- Prevents awkward character-level line breaks
- Better typography control
Example issue:
<!-- Without smart wrap -->
<span class="char">H</span>
<span class="char">e</span> <!-- Line break here! -->
<span class="char">l</span>
<span class="char">l</span>
<span class="char">o</span>
<!-- "He" on one line, "llo" on next - ugly! -->
<!-- With smart wrap (SplitText) -->
<span style="white-space: nowrap;">
<span class="char">H</span>
<span class="char">e</span>
<span class="char">l</span>
<span class="char">l</span>
<span class="char">o</span>
</span>
<!-- Word stays together! ✅ -->Impact Level: 🟠 MEDIUM - Annoying edge case.
Why it matters:
- Handles
<strong>,<em>,<a>spanning multiple lines - Prevents vertical expansion of lines
Example:
<!-- Input -->
<p>This is <strong>important bold text</strong> here.</p>
<!-- If "bold text" wraps across 2 lines, SplitText subdivides it -->
<!-- CharWrapper strips it or breaks it ❌ -->Impact Level: 🟠 MEDIUM - Common in CMS content.
Why it matters:
- Clean CSS-based animations
- No JS animation needed for simple effects
SplitText:
new SplitText('.text', { propIndex: true });.char {
animation-delay: calc(var(--char) * 0.05s);
}CharWrapper:
// ❌ Not supported, must use GSAP or manual stylingImpact Level: 🟠 MEDIUM - Nice-to-have for CSS animations.
Why it matters:
- Split by custom characters (e.g.,
-or|) - Internationalization needs
Impact Level: 🟢 LOW - Rare use case.
Why it matters:
- Skip certain elements (e.g.,
<sup>,<sub>)
Impact Level: 🟢 LOW - Can work around with data attributes.
| Feature Category | CharWrapper 2.0 | GSAP SplitText | Winner |
|---|---|---|---|
| Basic char/word split | ✅ Good | ✅ Excellent | Tie |
| Line splitting | ❌ None | ✅ Excellent | SplitText |
| Accessibility | ✅ Excellent | ✅ Excellent | Tie |
| Performance | ✅ Good | ✅ Excellent | SplitText |
| File size | 13KB (minified) | 14KB | Tie |
| TypeScript | JSDoc with TS compatibility | Native TS | SplitText |
| Auto-responsiveness | ❌ Manual | ✅ Auto | SplitText |
| Masking | ❌ Manual | ✅ Built-in | SplitText |
| API simplicity | Good | Excellent | SplitText |
| Documentation | Excellent | Excellent | Tie |
| Examples | 17+ demos | Many | Tie |
| Price | Free | Free (since v3.13) | Tie |
| Dependencies | None | GSAP core | CharWrapper |
| Custom config | More verbose | Concise | SplitText |
| Character grouping | ✅ Advanced | ❌ None | CharWrapper |
| Animation presets | ✅ Built-in | ❌ None | CharWrapper |
| Text transitions | ✅ Available | ❌ None | CharWrapper |
| Data attribute selection | ✅ Available | ❌ None | CharWrapper |
- ✅ You need line splitting (most professional work)
- ✅ Building responsive sites with web fonts
- ✅ Need mask/reveal effects
- ✅ Want auto-resplit on viewport changes
- ✅ Working on client projects (proven, supported)
- ✅ Need advanced features (deep slice, custom delimiters, etc.)
- ✅ You only need chars/words (no lines)
- ✅ Want advanced character grouping capabilities
- ✅ Need animation presets for quick effects
- ✅ Want text transitions between different content
- ✅ Need data attribute-driven content organization
- ✅ Want TypeScript compatibility with JSDoc
- ✅ Want comprehensive accessibility features
- ✅ Want zero dependencies (doesn't require GSAP)
- ✅ Learning/educational purposes
- ✅ Want full control of implementation
- ✅ Building a custom solution
To make CharWrapper competitive with SplitText, add these features in priority order:
- Line splitting algorithm - Core feature gap
- autoSplit - Essential for modern web
- Masking support - Professional animations
- Reduce file size - Tree-shaking, minification
- Smart wrap - Better typography
- Deep slice - Nested element handling
- CSS variable indices - Modern CSS integration
- Callbacks (onSplit, onRevert)
- Custom word delimiters
- Ignore selectors
- Special char handling
GSAP SplitText remains the clear winner for professional production use, especially when line splitting is needed. It's:
- 🔴 More feature-complete (14+ advanced features)
- 🔴 Auto-responsive
- 🔴 Written in TypeScript
- 🔴 Industry standard
CharWrapper 2.0 is excellent for:
- 📚 Character/word-only animations
- 🎨 Advanced character grouping capabilities
- 🔄 Text transitions between different content
- 🎬 Built-in animation presets
- 📋 Data attribute-driven content organization
- 🚫 Zero dependencies (doesn't require GSAP)
- 🎯 Full control of implementation
- 📖 Educational purposes
CharWrapper 2.0 is well-engineered with modern practices, comprehensive accessibility features, and unique capabilities like character grouping and animation presets. While GSAP SplitText remains the go-to for professional work requiring line splitting, CharWrapper provides a capable alternative for character/word-based animations with additional features.
If you need line splitting: Use SplitText. If you want advanced grouping, animation presets, or zero dependencies: CharWrapper is excellent!
The most valuable features CharWrapper lacks:
- 🔴 Line splitting (dealbreaker for most pros)
- 🟡 Auto-split responsiveness (quality of life)
- 🟡 Masking/reveal effects (professional animations)
Bottom line: Both libraries serve different needs. GSAP SplitText for professional line-splitting work, CharWrapper for advanced character manipulation with extra features. Both are production-ready tools! 🚀
Get up and running with CharWrapper in under 5 minutes! 🚀
Double-click or open in your browser:
examples/index.html
This showcases all 4 example types. Click any card to see it in action!
Create a new HTML file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My First CharWrapper Animation</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
h1 {
font-size: 4rem;
color: white;
}
.char {
display: inline-block;
}
</style>
</head>
<body>
<h1 class="text">Hello World</h1>
<!-- Load GSAP from CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<!-- Load CharWrapper -->
<script type="module">
import CharWrapper from './src/CharWrapper.js';
// Create wrapper
const wrapper = new CharWrapper('.text', {
wrap: { chars: true }
});
// Wrap the text
const { chars } = wrapper.wrap();
// Animate!
gsap.from(chars, {
opacity: 0,
y: 50,
rotation: -180,
stagger: 0.05,
duration: 1,
ease: 'back.out(1.7)'
});
</script>
</body>
</html>That's it! Open this file in your browser. ✨
import CharWrapper from './src/CharWrapper.js';
const wrapper = new CharWrapper('.text', {
wrap: { chars: true }
});
const { chars } = wrapper.wrap();
gsap.from(chars, {
opacity: 0,
stagger: 0.05
});gsap.from(chars, {
opacity: 0,
y: 30,
stagger: 0.03,
ease: 'power2.out'
});gsap.from(chars, {
scale: 0,
rotation: 360,
stagger: 0.04,
ease: 'back.out(2)'
});const wrapper = new CharWrapper('.text', {
wrap: { words: true, chars: true }
});
const { words, chars } = wrapper.wrap();
// Animate words
gsap.from(words, {
opacity: 0,
x: -50,
stagger: 0.1
});
// Or animate individual characters
gsap.from(chars, {
opacity: 0,
stagger: 0.02
});const wrapper = new CharWrapper('.text', {
wrap: { chars: true },
enumerate: { chars: true } // Adds .char-001, .char-002, etc.
});
const { chars } = wrapper.wrap();
// Now you can target specific characters in CSS!CSS:
.char-001 { color: red; }
.char-002 { color: blue; }
.char-003 { color: green; }const { chars } = wrapper.wrap();
chars.forEach(char => {
char.addEventListener('mouseenter', () => {
gsap.to(char, { scale: 1.5, duration: 0.3 });
});
char.addEventListener('mouseleave', () => {
gsap.to(char, { scale: 1, duration: 0.3 });
});
});import CharWrapper from './src/CharWrapper.js';
gsap.registerPlugin(ScrollTrigger);
const wrapper = new CharWrapper('.text', {
wrap: { chars: true }
});
const { chars } = wrapper.wrap();
gsap.from(chars, {
opacity: 0,
y: 30,
stagger: 0.02,
scrollTrigger: {
trigger: '.text',
start: 'top 80%',
end: 'top 50%',
scrub: 1
}
});new CharWrapper(selector, {
// What to wrap
wrap: {
chars: true, // Wrap individual characters
words: false, // Wrap words
spaces: false, // Wrap space characters
specialChars: false // Wrap !?.,; etc.
},
// Add numbered classes (.char-001, .char-002)
enumerate: {
chars: false, // Enable char numbering
words: false, // Enable word numbering
includeSpaces: false, // Include spaces in count
includeSpecialChars: false // Include special chars in count
},
// CSS class names
classes: {
char: 'char', // Base char class
word: 'word', // Base word class
space: 'char--space', // Space class
special: 'char--special', // Special char class
regular: 'char--regular' // Regular char class
},
// HTML tags
tags: {
char: 'span', // span, div, i, em, strong, mark
word: 'span'
},
// Space replacement
replaceSpaceWith: '\xa0', // Non-breaking space
// Performance (usually keep defaults)
performance: {
useBatching: true, // DocumentFragment batching
cacheSelectors: true // Cache DOM queries
}
});const wrapper = new CharWrapper('.text', config);
// Wrap the text
const { chars, words } = wrapper.wrap();
// Get elements later
const allChars = wrapper.getChars();
const allWords = wrapper.getWords();
// Get specific element
const firstChar = wrapper.getChar(0);
const secondWord = wrapper.getWord(1);
// Filter by type
const regularChars = wrapper.getCharsByType('regular');
const spaces = wrapper.getCharsByType('space');
const specialChars = wrapper.getCharsByType('special');
// Check state
if (wrapper.isWrapped()) {
// ...
}
// Unwrap (restore original)
wrapper.unwrap();
// Rewrap (unwrap + wrap)
wrapper.rewrap();
// Clean up (important!)
wrapper.destroy();// In single-page apps, always clean up!
window.addEventListener('beforeunload', () => {
wrapper.destroy();
});// Quick one-liner
const wrapper = CharWrapper.create('.text', { wrap: { chars: true } });.char {
display: inline-block;
transition: all 0.3s;
}
.char:hover {
color: #ff6b6b;
transform: translateY(-5px);
}const { chars } = wrapper.wrap();
// Only animate letters (not spaces)
const regularChars = wrapper.getCharsByType('regular');
gsap.from(regularChars, { opacity: 0, stagger: 0.05 });const wrappers = CharWrapper.wrapMultiple(
['.title', '.subtitle', '.description'],
{ wrap: { chars: true } }
);
// Clean up all at once
wrappers.forEach(w => w.destroy());/* Without this, wrapped chars won't flow correctly */
.char {
display: inline-block; /* Add this! */
}// Memory leak in SPAs!
const wrapper = new CharWrapper('.text', config);
wrapper.wrap();
// ... never destroyed✅ Do: Always clean up
const wrapper = new CharWrapper('.text', config);
wrapper.wrap();
// Later...
wrapper.destroy();wrapper.wrap();
wrapper.wrap(); // Error! Already wrapped✅ Do: Use rewrap() or unwrap first
wrapper.wrap();
wrapper.rewrap(); // Correct!
// OR
wrapper.unwrap();
wrapper.wrap(); // Also correct!- ✅ You've created your first animation!
- 🎨 Explore
examples/folder for more patterns - 📖 Read
README.mdfor complete API docs - 🔄 Check
MIGRATION_GUIDE.mdif upgrading from v1.0 - 💡 Experiment with different GSAP eases and effects!
- Check the examples:
examples/index.html - Read the full docs:
README.md - See the migration guide:
MIGRATION_GUIDE.md - Review the summary:
SUMMARY.md
CharWrapper is distributed with multiple build formats to support different environments:
| File | Format | Size | Use Case |
|---|---|---|---|
dist/charwrapper.min.js |
IIFE | ~13KB | Direct browser inclusion |
dist/charwrapper.cjs.min.js |
CommonJS | ~13KB | Node.js compatibility |
dist/charwrapper.js |
IIFE | ~36KB | Development with source maps |
dist/charwrapper.cjs.js |
CommonJS | ~36KB | Node.js compatibility |
dist/esm/ |
ES Modules | ~40KB | NPM package (tree-shakeable) |
When using bundlers, CharWrapper properly exports different formats based on your environment:
// In bundler environments (Webpack, Vite, etc.) - uses ES modules
import CharWrapper from 'charwrapper';
// In browsers with native modules - uses ES modules
<script type="module">
import CharWrapper from 'charwrapper';
</script>
// In browsers without modules - uses IIFE version
<script src="https://cdn.jsdelivr.net/npm/charwrapper@latest/dist/charwrapper.min.js"></script>CharWrapper 2.0 is designed to be simple yet powerful.
Happy animating! 🚀✨