Skip to content

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.

License

Notifications You must be signed in to change notification settings

azkacrows/Typewriter-Component

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

5 Commits
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Typewriter Component

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.

🏒 Learning-Oriented Production Checklist

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

πŸ“– General Parameters

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.

Temporal and Kinetic Controls

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.

Event-Driven Callbacks

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.

HTML Rendering and Safety Protocols

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.

Line Management Parameters

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).

πŸš€ Deployment Recommendations

For High-Traffic Apps:

<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);
    }}
/>

For Security-Critical Apps:

<Typewriter
    texts={userContent}
    enableHtml={true}
    sanitizeHtml={true} // Always true for user content
    onError={handleSecurityError}
/>

For Performance-Critical Apps:

<Typewriter
    texts={staticContent}
    enableHtml={true}
    maxCacheSize={200} // Larger cache for repeated content
    typeSpeed={0.03} // Faster animation for better UX
/>

For Real-Time Applications:

<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',
        });
    }}
/>

⚠️ Implementation Notes

Bundle Size:

  • Dependencies: Requires GSAP (must install separately)
  • Recommendation: Tree-shake unused features if needed

Browser Support:

  • Modern browsers: Full ES6+ support required
  • Legacy: May need polyfills for Map/Set in older browsers

Content Limitations:

  • Max content length: Configurable via maxHtmlLength prop
  • HTML nesting: Supports reasonable nesting levels
  • Animation performance: Depends on content complexity and browser

Basic Usage Examples

1. Copy it then Import the Component

example:

import { Typewriter } from '@/components/animations/Typewriter';

1.1 Simple Text Animation

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>
    );
}

1.2 Single Text Without Loop

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>
    );
}

2. HTML Styling Examples

2.1 Code Syntax Highlighting with HTML

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>
    );
}

2.2 Rich Text with Mixed Styling

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>
    );
}

3. Internationalization Examples

3.1 Usage with next-intl Translations

'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>
    );
}

3.2 Custom Translation Function

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>
    );
}

4. Advanced Features & Callbacks

4.1 Event Handling and State Management

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>
    );
}

4.2 Error Handling and Monitoring

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>
    );
}

5. Performance & Security Examples

5.1 High-Performance Configuration

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>
    );
}

5.2 Secure HTML Content Handling

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>
    );
}

6. Styling Variations

6.1 No Cursor Variation

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>
    );
}

6.2 Required CSS Styles

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>
    );
}

6.3 Error State Styling

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>
    );
}

7. Installation & Setup

7.1 Dependencies

npm install gsap
# or
yarn add gsap

7.2 CSS Setup

Add 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;

8. Complete Props Reference

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: ' ')
}

9. Best Practices & Tips

βœ… DO:

  • Use enableHtml only 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 onError callback for production monitoring
  • Use translation keys for internationalized applications
  • Test with various content sizes and complexity
  • Use newLineIterable for chat interfaces, tutorials, and step-by-step content
  • Set reasonable iterableDelay (0.5-2 seconds) for good UX
  • Use initialDelay to create anticipation or wait for page load
  • Position cursor at the end of active lines with cursor={true}
  • Use normalizeLines={true} with minLineLength for 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 onComplete callback for user interactions
  • Use visible paddingChar unless intentionally showing padding

πŸ”§ Performance Tips:

  • Adjust maxCacheSize based on your content variety
  • Consider disabling loop for very long content
  • Monitor performance with onError callback
  • Use reasonable maxHtmlLength limits
  • 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 sanitizeHtml for user-generated content
  • Validate that translation functions return safe content
  • Consider implementing Content Security Policy (CSP) headers
  • Monitor errors for potential security issues

Accessibility Improvements

  • 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

πŸ› Known Issues Fixed

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.

🀝 Contributing

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:

  1. Repository Forking: Initiate your contribution by forking the repository and creating a distinct branch from the main branch.
  2. 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.
  3. Adherence to Standards: Maintain fidelity to established coding conventions, architectural principles, and stylistic norms to promote uniformity and long-term maintainability.
  4. 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.


πŸ“œ License

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.

About

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.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published