At first I was bored and made a typewriter effect, but as time went on I wanted to add other features like internationalization and finally I wanted to be able to style the text in this component. This was built to continue learning TypeScript without any AI assistance except for building the documentation - this take 2 days and a lot of trial and error.
This component was created as part of my journey learning TypeScript. I tried to include features and considerations that are usually important in production software. While not all items have been tested at scale, the goal was to practice implementing them as realistically as possible.
| Feature | Status | Notes |
|---|---|---|
| **** | β | XSS prevention, content sanitization |
| Performance | β | Caching, memory management, optimization |
| Error Handling | β | Comprehensive error recovery |
| Accessibility | β | ARIA attributes, screen reader support |
| TypeScript | β | Full type safety |
| Testing Ready | β | Data attributes for testing |
| Monitoring | β | Error callbacks for tracking |
| Memory Safety | β | Proper cleanup, no leaks |
| Browser Support | β | Modern browsers with ES6+ support |
| π Multi-mode | β | Traditional + NewLine modes |
| π Loop Safety | β | Fixed infinite loop bugs |
| Prop | Type | Default | Description |
|---|---|---|---|
| texts | TypewriterItem[] (string or { key: string }) |
required | The sequence of textual elements to animate. Accepts either raw strings or keyed objects. |
| className | string |
'' |
An optional CSS class applied to the container <span> for targeted styling. |
| cursor | boolean |
false |
If enabled, appends a blinking cursor to the terminal position of the rendered string. |
| loop | boolean |
false |
If enabled, perpetually cycles through the full set of texts in succession. |
| Prop | Type | Default | Description |
|---|---|---|---|
| typeSpeed | number |
0.1 |
Specifies the interval, in seconds per character, for text construction. The value must be strictly positive. |
| deleteSpeed | number |
0.05 |
Specifies the interval, in seconds per character, for text deletion. The value must be strictly positive. |
| delayBetween | number |
1 |
Governs the interstitial pause (in seconds) after a sequence is typed before its deletion or progression. |
| initialDelay | number |
0 |
Establishes an onset latency (in seconds) before the first animation commences. Must be nonnegative. |
| iterableDelay | number |
0.5 |
Determines the temporal spacing (in seconds) between successive lines under newLineIterable mode. Nonnegative. |
| Prop | Type | Default | Description |
|---|---|---|---|
| onComplete | () => void |
undefined |
Invoked precisely once when all texts have been rendered (operative only if loop is disabled). |
| onTextChange | (text: string, index: number) => void |
undefined |
Executed whenever the visible string transitions, with arguments for the content and index. |
| onError | (error: Error) => void |
undefined |
Activated upon error detection within the animation process, returning the error object. |
| Prop | Type | Default | Description |
|---|---|---|---|
| enableHtml | boolean |
false |
Permits the embedding and rendering of raw HTML tags within the animated text sequence. |
| sanitizeHtml | boolean |
true |
Implements sanitization to preclude injection of unsafe or malicious HTML constructs. |
| maxHtmlLength | number |
10000 |
Imposes an upper bound (in characters) on the permissible size of an HTML fragment. |
| maxCacheSize | number |
50 |
Defines the maximum capacity of the internal cache utilized for storing parsed HTML entities. |
| Prop | Type | Default | Description |
|---|---|---|---|
| newLineIterable | boolean |
false |
If enabled, appends successive texts as new lines rather than overwriting the existing content. |
| minLineLength | number |
0 |
Ensures each rendered line attains a specified minimum length, achieved by appending padding characters as needed. |
| paddingChar | string |
' ' |
Determines the padding character to be inserted when enforcing minLineLength. |
| normalizeLines | boolean |
false |
If enabled, automatically normalizes all lines to identical length (effective only with newLineIterable enabled). |
<Typewriter
texts={content}
enableHtml={true}
maxCacheSize={100} // Increase cache for better performance
maxHtmlLength={5000} // Adjust based on your content
onError={(error) => {
// Send to your error tracking service
analytics.trackError('typewriter_error', error);
}}
/><Typewriter
texts={userContent}
enableHtml={true}
sanitizeHtml={true} // Always true for user content
onError={handleSecurityError}
/><Typewriter
texts={staticContent}
enableHtml={true}
maxCacheSize={200} // Larger cache for repeated content
typeSpeed={0.03} // Faster animation for better UX
/><Typewriter
texts={liveMessages}
newLineIterable={true}
initialDelay={0}
iterableDelay={0.3}
typeSpeed={0.02}
cursor={true}
onTextChange={(text, index) => {
// Send analytics for each message completion
analytics.track('message_displayed', { index, content: text });
}}
onError={(error) => {
errorTracking.captureException(error, {
context: 'realtime_typewriter',
mode: 'newLineIterable',
});
}}
/>- Dependencies: Requires GSAP (must install separately)
- Recommendation: Tree-shake unused features if needed
- Modern browsers: Full ES6+ support required
- Legacy: May need polyfills for Map/Set in older browsers
- Max content length: Configurable via
maxHtmlLengthprop - HTML nesting: Supports reasonable nesting levels
- Animation performance: Depends on content complexity and browser
example:
import { Typewriter } from '@/components/animations/Typewriter';export default function BasicExample() {
return (
<h1 className="text-2xl text-center mt-20">
I am a{' '}
<Typewriter
texts={['developer', 'designer', 'freelancer']}
typeSpeed={0.06}
deleteSpeed={0.03}
delayBetween={1.2}
className="text-blue-600 font-semibold"
loop={true}
/>
</h1>
);
}export default function SingleTextExample() {
return (
<div className="text-center mt-20">
<Typewriter
texts={['Welcome to our website!']}
typeSpeed={0.08}
loop={false}
cursor={false}
className="text-3xl font-bold text-gray-800"
/>
</div>
);
}export default function CodeExample() {
const codeTexts = [
`<span class="text-green-400 italic">// 404 page not found.</span>`,
`<span class="text-purple-400 font-semibold">if</span><span class="text-gray-300"> (</span><span class="text-orange-400">!</span><span class="text-blue-300 italic">found</span><span class="text-gray-300">) {</span>`,
`<span class="pl-8 text-purple-400 font-semibold">throw</span><span class="text-gray-300"> (</span><span class="text-yellow-300">"(β―Β°β‘Β°)β―οΈ΅ β»ββ»"</span><span class="text-gray-300">);</span>`,
`<span class="text-gray-300">}</span>`,
`<span class="text-green-400 italic">// <a href="/" class="text-blue-400 underline hover:text-blue-300">Go home!</a></span>`,
];
return (
<div className="bg-gray-900 p-6 rounded-lg font-mono">
<Typewriter
texts={codeTexts}
newLineIterable={true}
enableHtml={true}
sanitizeHtml={true}
typeSpeed={0.05}
deleteSpeed={0.02}
delayBetween={2}
iterableDelay={0.8}
loop={false}
className="text-base leading-relaxed block whitespace-pre-line"
/>
</div>
);
}export default function RichTextExample() {
const richTexts = [
`<span class="text-2xl font-bold text-blue-600">Hello!</span>`,
`<div class="text-lg">
I'm a <span class="font-semibold text-purple-600">Full Stack Developer</span>
</div>`,
`<div class="text-lg">
I love <span class="italic text-green-600">React</span> and
<span class="font-mono text-orange-600">TypeScript</span>
</div>`,
];
return (
<div className="text-center mt-20">
<Typewriter
texts={richTexts}
enableHtml={true}
typeSpeed={0.07}
delayBetween={3}
loop={true}
/>
</div>
);
}'use client';
import { useTranslations } from 'next-intl';
export default function WithTranslationsExample() {
const t = useTranslations();
return (
<h1 className="text-2xl text-center mt-20">
{t('home.intro')}{' '}
<Typewriter
texts={[
'developer',
{ key: 'home.roles.designer' },
{ key: 'home.roles.freelancer' },
]}
translateFn={t}
typeSpeed={0.06}
deleteSpeed={0.03}
delayBetween={1.2}
className="text-blue-600 font-semibold"
loop={true}
/>
</h1>
);
}export default function CustomTranslationExample() {
const translations = {
'role.developer': 'Entwickler',
'role.designer': 'Designer',
'role.freelancer': 'Freiberufler',
};
const customTranslate = (key: string) => {
return translations[key as keyof typeof translations] || key;
};
return (
<h1 className="text-2xl text-center mt-20">
Ich bin ein{' '}
<Typewriter
texts={[
{ key: 'role.developer' },
{ key: 'role.designer' },
{ key: 'role.freelancer' },
]}
translateFn={customTranslate}
typeSpeed={0.08}
deleteSpeed={0.04}
delayBetween={1.5}
className="text-green-600 font-bold"
loop={true}
/>
</h1>
);
}import { useState } from 'react';
export default function CallbackExample() {
const [isComplete, setIsComplete] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const [currentText, setCurrentText] = useState('');
return (
<div className="text-center mt-20">
<h1 className="text-2xl mb-4">
Welcome,{' '}
<Typewriter
texts={['visitor', 'friend', 'developer']}
typeSpeed={0.1}
deleteSpeed={0.05}
delayBetween={2}
className="text-purple-600 font-semibold"
loop={false}
onComplete={() => {
setIsComplete(true);
console.log('Animation completed!');
}}
onTextChange={(text, index) => {
setCurrentText(text);
setCurrentIndex(index);
}}
/>
</h1>
<div className="text-sm text-gray-600 mt-4">
<p>Current text: "{currentText}"</p>
<p>Current index: {currentIndex}</p>
{isComplete && (
<p className="text-green-600 font-semibold animate-fade-in">
β Animation completed!
</p>
)}
</div>
</div>
);
}import { useState } from 'react';
export default function ErrorHandlingExample() {
const [errors, setErrors] = useState<string[]>([]);
const [isHealthy, setIsHealthy] = useState(true);
const handleError = (error: Error) => {
console.error('Typewriter error:', error);
setErrors((prev) => [...prev, error.message]);
setIsHealthy(false);
// analytics.trackError('typewriter_error', error);
};
const problematicTexts = [
'Normal text',
{ key: 'nonexistent.translation.key' },
`<script>alert('xss')</script>Safe content after malicious attempt`,
{ key: 'another.missing.key' },
];
return (
<div className="text-center mt-20">
<div className="mb-4">
Status:{' '}
<span className={`font-semibold ${isHealthy ? 'text-green-600' : 'text-red-600'}`}>
{isHealthy ? 'β Healthy' : 'β Has Errors'}
</span>
</div>
<Typewriter
texts={problematicTexts}
enableHtml={true}
sanitizeHtml={true}
translateFn={(key) => `Missing: ${key}`}
loop={true}
className="text-lg"
onError={handleError}
/>
{errors.length > 0 && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg text-left">
<h3 className="font-semibold text-red-800 mb-2">Errors:</h3>
<ul className="text-sm text-red-700 space-y-1">
{errors.map((error, index) => (
<li key={index}>β’ {error}</li>
))}
</ul>
</div>
)}
</div>
);
}export default function PerformanceExample() {
const largeParagraphs = [
'This is a substantial paragraph that demonstrates how the Typewriter component handles larger amounts of text efficiently through its caching system...',
'Another piece of content that shows the optimization features in action with proper memory management...',
'The component uses intelligent caching to improve performance for repeated content...',
];
return (
<div className="max-w-4xl mx-auto mt-20 p-6">
<Typewriter
texts={largeParagraphs}
typeSpeed={0.02}
deleteSpeed={0.01}
delayBetween={3}
loop={true}
maxCacheSize={100}
maxHtmlLength={5000}
className="text-lg leading-relaxed text-gray-700"
/>
</div>
);
}export default function SecurityExample() {
const potentiallyDangerousContent = [
`<span class="text-blue-600">Safe content</span>`,
`<script>alert('This will be removed')</script><span class="text-green-600">But this is safe</span>`,
`<div class="text-purple-600">Styled content</div>`,
];
return (
<div className="text-center mt-20">
<h2 className="text-xl font-semibold mb-4">Security Demonstration</h2>
<p className="text-sm text-gray-600 mb-6">
All potentially malicious content is automatically sanitized
</p>
<Typewriter
texts={potentiallyDangerousContent}
enableHtml={true}
sanitizeHtml={true}
typeSpeed={0.05}
loop={true}
className="text-lg"
onError={(error) => {
console.warn('Security issue detected and handled:', error);
}}
/>
</div>
);
}export default function NoCursorExample() {
return (
<div className="text-center mt-20">
<h1 className="text-3xl font-light">
<Typewriter
texts={['Clean text without blinking cursor']}
cursor={false}
loop={false}
typeSpeed={0.08}
className="text-gray-800"
/>
</h1>
</div>
);
}Add these styles to your global CSS file:
/* Cursor animation for typewriter effect */
.typewriter-cursor::after {
content: '|';
animation: typewriter-blink 1s steps(1) infinite;
margin-left: 2px;
color: currentColor;
}
@keyframes typewriter-blink {
0%,
50% {
opacity: 1;
}
50.01%,
100% {
opacity: 0;
}
}
/* Error state styling */
.typewriter-error {
color: #ef4444; /* Red color for error state */
font-style: italic;
opacity: 0.8;
}
/* Optional: Custom cursor variations */
.custom-cursor::after {
content: 'β'; /* Block cursor */
animation: typewriter-blink 1s steps(1) infinite;
margin-left: 2px;
color: currentColor;
}
.custom-cursor-underscore::after {
content: '_'; /* Underscore cursor */
animation: typewriter-blink 1s steps(1) infinite;
color: currentColor;
}export default function CustomCursorExample() {
return (
<div className="text-center mt-20 space-y-4">
{/* Block cursor */}
<div>
<Typewriter
texts={['Custom block cursor', 'Looks bold!', 'Try it out']}
cursor={false}
className="custom-cursor text-2xl font-semibold text-blue-600"
loop={true}
/>
</div>
{/* Underscore cursor */}
<div>
<Typewriter
texts={['Underscore cursor', 'Classic style', 'Retro vibes']}
cursor={false}
className="custom-cursor-underscore text-xl text-green-600"
loop={true}
/>
</div>
</div>
);
}The component automatically applies the typewriter-error class when errors occur:
export default function ErrorStateExample() {
const [forceError, setForceError] = useState(false);
// Example of component with error state
const problematicTexts = forceError ? [null, undefined] : ['Normal text'];
return (
<div className="text-center mt-20">
<button
onClick={() => setForceError(!forceError)}
className="mb-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
{forceError ? 'Fix Error' : 'Trigger Error'}
</button>
<div>
<Typewriter
texts={problematicTexts as any}
typeSpeed={0.05}
className="text-lg"
onError={(error) => console.log('Error:', error.message)}
/>
</div>
</div>
);
}npm install gsap
# or
yarn add gsapAdd the required CSS to your global stylesheet or component:
/* Required: Add this to your global.css or equivalent */
.typewriter-cursor::after {
content: '|';
animation: typewriter-blink 1s steps(1) infinite;
margin-left: 2px;
color: currentColor;
}
@keyframes typewriter-blink {
0%,
50% {
opacity: 1;
}
50.01%,
100% {
opacity: 0;
}
}
/* Error state styling */
.typewriter-error {
color: #ef4444;
font-style: italic;
opacity: 0.8;
}
/* π NEW: Better line spacing for multi-line mode */
[data-newline-iterable='true'] {
white-space: pre-line;
line-height: 1.6;
}
/* Optional: Custom cursor variations */
.custom-cursor::after {
content: 'β'; /* Block cursor */
animation: typewriter-blink 1s steps(1) infinite;
margin-left: 2px;
color: currentColor;
}
.custom-cursor-underscore::after {
content: '_'; /* Underscore cursor */
animation: typewriter-blink 1s steps(1) infinite;interface TypewriterProps {
texts: (string | { key: string })[];
typeSpeed?: number; // Animation speed for typing (default: 0.1)
deleteSpeed?: number; // Animation speed for deleting (default: 0.05)
delayBetween?: number; // Delay between text changes in traditional mode (default: 1)
loop?: boolean; // Whether to loop through texts (default: false)
cursor?: boolean; // Show blinking cursor (default: false)
className?: string; // CSS classes to apply
enableHtml?: boolean; // Enable HTML content rendering (default: false)
sanitizeHtml?: boolean; // Sanitize HTML content (default: true)
maxHtmlLength?: number; // Maximum HTML content length (default: 10000)
translateFn?: (key: string) => string; // Translation function for text keys
onComplete?: () => void; // Callback when animation completes
onTextChange?: (text: string, index: number) => void; // Callback on text change
onError?: (error: Error) => void; // Error handling callback
maxCacheSize?: number; // Maximum cache size for parsed HTML (default: 50)
// π NEW PROPS
newLineIterable?: boolean; // Add new lines instead of deleting (default: false)
initialDelay?: number; // Delay in seconds before starting animation (default: 0)
iterableDelay?: number; // Delay in seconds between lines in newLineIterable mode (default: 0.5)
// π TEXT FORMATTING PROPS
normalizeLines?: boolean; // Auto-normalize line lengths in newLineIterable mode (default: false)
minLineLength?: number; // Minimum line length for consistent spacing (default: 0)
paddingChar?: string; // Character to use for padding short lines (default: ' ')
}β DO:
- Use
enableHtmlonly when you need HTML styling - Always keep
sanitizeHtml={true}for user-generated content - Use faster
typeSpeed(0.02-0.05) for better user experience - Implement
onErrorcallback for production monitoring - Use translation keys for internationalized applications
- Test with various content sizes and complexity
- Use
newLineIterablefor chat interfaces, tutorials, and step-by-step content - Set reasonable
iterableDelay(0.5-2 seconds) for good UX - Use
initialDelayto create anticipation or wait for page load - Position cursor at the end of active lines with
cursor={true} - Use
normalizeLines={true}withminLineLengthfor mixed text lengths - Choose appropriate
paddingChar(' ' for invisible, '.' for visible)
β DON'T:
- Disable HTML sanitization for untrusted content
- Use very slow typeSpeed (>0.2) - users may lose interest
- Put excessive content in a single text item
- Forget error handling in production environments
- Use overly complex nested HTML structures
- Use extremely short
iterableDelay(<0.2s) - creates rushed feeling - Forget to handle the
onCompletecallback for user interactions - Use visible
paddingCharunless intentionally showing padding
π§ Performance Tips:
- Adjust
maxCacheSizebased on your content variety - Consider disabling loop for very long content
- Monitor performance with
onErrorcallback - Use reasonable
maxHtmlLengthlimits - Use
newLineIterable={false}(traditional mode) for simple text cycling - Use
newLineIterable={true}for content that should accumulate - Monitor memory usage with large text arrays (>50 items)
- Implement proper error handling for production apps
π‘οΈ Security Tips:
- Never disable
sanitizeHtmlfor user-generated content - Validate that translation functions return safe content
- Consider implementing Content Security Policy (CSP) headers
- Monitor errors for potential security issues
- The component now has better ARIA support for multi-line content
- Screen readers handle the new line mode more gracefully
- Proper semantic HTML structure is maintained
| Issue | Status | Fix Description |
|---|---|---|
| Infinite loop in certain conditions | β FIXED | Improved index boundary checking |
| Memory leaks with rapid re-mounts | β FIXED | Enhanced cleanup in useEffect |
| React key warnings in console | β FIXED | Better key generation for mapped elements |
| Cursor positioning in HTML mode | β FIXED | Improved cursor placement logic |
| Loop logic inconsistencies | β FIXED | Simplified and more predictable loop behavior |
| TypeScript type inference issues | β FIXED | Enhanced type definitions |
Bottom line: This component was built as part of my TypeScript learning journey. It includes several features inspired by production-ready software, such as error handling, security considerations, and performance optimizations. While it may not yet be fully tested for professional use, it can serve as a solid starting point for projects that need animated text with HTML support.
The development of this project is conceived as an open, collaborative endeavor, and contributions from the scholarly and professional community are strongly encouraged. In order to maintain the rigor and coherence of the codebase, contributors are asked to adhere to the following protocol:
- Repository Forking: Initiate your contribution by forking the repository and creating a distinct branch from the
mainbranch. - Commit Discipline: Ensure that all modifications are accompanied by precise and analytically descriptive commit messages that capture both the intent and the technical nuance of the change.
- Adherence to Standards: Maintain fidelity to established coding conventions, architectural principles, and stylistic norms to promote uniformity and long-term maintainability.
- Pull Request Submission: Submit a pull request (PR) that provides a substantive and well-structured exegesis of the proposed changes, including rationale, scope, and implications.
Scholarly engagement in the form of theoretical insights, identification of latent defects, or the proposition of new research-informed features is deeply valued. Prospective contributors are invited to open an issue to engage in discursive evaluation of significant modifications prior to the submission of a PR.
This project is disseminated under the GNU General Public License v3.0 (GPL-3.0).
The license confers upon the user the rights to employ, adapt, and redistribute the software, contingent upon strict adherence to the provisions articulated within the GNU GPL-3.0 framework. For comprehensive legal and philosophical exposition, please consult the LICENSE file.