| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Modern text wrapper library with TypeScript support, designed for seamless integration with GSAP and other animation libraries.
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:
<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 root/text records by data-root-order and data-set-order
},
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:
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: {
rootSet: false, // Add numbered classes with the root-set class prefix
chars: false, // Add numbered classes (.char-001, .char-002)
words: false, // Add numbered classes to words
attributeSets: false, // Add numbered classes with data-set-char-class / data-set-word-class
includeSpaces: false, // Include spaces in enumeration count
includeSpecialChars: false // Include special chars in enumeration count
},
// CSS Classes - Customize the class names used
classes: {
rootSet: 'belongs-to-root-set', // Class applied to every wrapped character
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
dataAttributes: {
rootSet: 'rootSet', // data-root-set
rootOrder: 'rootOrder', // data-root-order
setName: 'setName', // data-set-name
setOrder: 'setOrder', // data-set-order
setCharClass: 'setCharClass', // data-set-char-class
setWordClass: 'setWordClass' // data-set-word-class
},
// Root Set Options
rootSet: {
customSets: {}, // JS-defined animation targets under each root set
exposeEmptyAttributeSets: false,
autoDetectDataAttributes: true
},
// 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 root/text records by data-root-order and data-set-order
},
// 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:
// 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:
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:
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('[data-root-set="profile"]', {
wrap: { chars: true }
});
// All text nodes inside this root set are now wrapped
const profileRootSet = wrapper.wrap();
// Animate all characters in document order
gsap.from(profileRootSet.chars, { opacity: 0, stagger: 0.02 });The data-set-name attributes provide semantic child sets inside the root set and enable features like custom classes, ordering, custom sets, and exclusion.
<div class="profile" data-root-set="profile">
<h1 data-set-name="first_name">John</h1>
<h1 data-set-name="last_name">Van der Slice</h1>
<!-- Mix in non-text elements -->
<div data-set-name="divider_line" class="divider"></div>
<p data-set-name="profession_1">composer</p>
<p data-set-name="profession_2">teacher</p>
<p data-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('[data-root-set="profile"]', {
wrap: { chars: true }
});
const profileRootSet = wrapper.wrap();
// Animate all characters in the order they appear in HTML
gsap.from(profileRootSet.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 } // Uses data-set-order inside this root
});When ordered: true, root sets returned by CharWrapper.wrapAll() are sorted by data-root-order, and text records inside a root set are sorted by nearest data-set-order.
<div class="profile" data-root-set="profile" data-root-order="1">
<!-- First name animates first -->
<h1 data-set-name="first_name" data-set-order="1">John</h1>
<!-- Last name animates second -->
<h1 data-set-name="last_name" data-set-order="2">Van der Slice</h1>
<!-- Professions animate in the order they appear -->
<p data-set-name="profession_1" data-set-order="3">composer</p>
<p data-set-name="profession_2" data-set-order="4">teacher</p>
<p data-set-name="profession_3" data-set-order="5">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 profileRootSet = wrapper.wrap();
const tl = gsap.timeline();
// First animate all text
tl.from(profileRootSet.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-set-char-class:
<div class="profile">
<!-- Add 'name-char' class to all characters in this element -->
<h1 data-set-name="first_name"
data-set-char-class="name-char">John</h1>
<!-- Add 'profession-char' class to all characters in this element -->
<p data-set-name="profession_1"
data-set-char-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-set-name="_exclude_":
<div class="profile">
<h1 data-set-name="name">John Doe</h1>
<!-- This will be skipped during wrapping -->
<div data-set-name="_exclude_">
<span>This text will NOT be wrapped</span>
</div>
<p data-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-set-name="first_name">John</h1>
<h1 data-set-name="last_name">Van der Slice</h1>
<!-- Animate a divider line -->
<div data-set-name="divider_line" class="divider"></div>
<p data-set-name="title">Lead Composer</p>
</div>const wrapper = new CharWrapper('[data-root-set="business-card"]', {
wrap: { chars: true }
});
const businessCardRootSet = wrapper.wrap();
// Create timeline
const tl = gsap.timeline();
// First name and last name appear
tl.from(businessCardRootSet.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-text-root="profile"]', {
wrap: { chars: true },
dataAttributes: {
rootSet: 'textRoot', // data-text-root
rootOrder: 'textRootOrder', // data-text-root-order
setName: 'profileItem', // data-profile-item
setOrder: 'profileItemOrder', // data-profile-item-order
setCharClass: 'profileChar', // data-profile-char
setWordClass: 'profileWord' // data-profile-word
}
});<!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" data-root-set="profile">
<h1 data-set-name="first_name">John</h1>
<h1 data-set-name="last_name">Van der Slice</h1>
<div data-set-name="divider_line" class="divider"></div>
<p data-set-name="profession_1">composer</p>
<p data-set-name="profession_2">teacher</p>
<p data-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:
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.
⚠️ Requires GSAP: Animation presets are optional GSAP-specific features. CharWrapper core is animation-agnostic and works with any animation library (anime.js, Framer Motion, etc.). To use presets, include GSAP separately:
<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:
Loop Animations:
Exit Animations:
Interactive:
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
⚠️ Requires GSAP: Text transitions are optional GSAP-specific features. CharWrapper core remains animation-agnostic. Include GSAP separately:
<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' });2. Sequential - Removes all, then adds all:
wrapper.transitionTo('Completely Different', { strategy: 'sequential' });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:
Wraps the text content.
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:
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:
Options:
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:
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();| 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 |
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>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.
Check out the examples folder for production-ready code:
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) |
| 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 | ⚠️ Possible but manual | ✅ 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 | ⚠️ Manual rewrap() | ✅ 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:
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:
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:
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:
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:
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:
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:
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:
Impact Level: 🟢 LOW - Rare use case.
Why it matters:
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 |
To make CharWrapper competitive with SplitText, add these features in priority order:
GSAP SplitText remains the clear winner for professional production use, especially when line splitting is needed. It's:
CharWrapper 2.0 is excellent for:
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:
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!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! 🚀✨
| Back | FazBrowse Home | New Git URL |