High-performance Blazor typewriter component. Optimized for AOT, trimming-friendly, and deterministic.
$ dotnet add package BlazorFastTypewriterA high-performance typewriter component for Blazor that animates text character-by-character with full HTML support. Built with .NET 10 features for optimal performance, AOT compilation, and aggressive trimming.
Why another typewriter? Shipping to WebAssembly or native AOT targets demands components that are deterministic, trimming-safe, and optimized from the first render. This component was built from the ground up with those goals, using modern .NET 10 features for maximum performance.

dotnet add package BlazorFastTypewriter
_Imports.razor@using BlazorFastTypewriter
<Typewriter Speed="100">
<p>Welcome to Blazor Fast Typewriter!</p>
</Typewriter>
That's it! CSS and JavaScript are automatically included via Blazor's static web assets.
ImmutableArray.Builder, collection expressions, pattern matching)RunAOTCompilation enabledSeek(), SeekToPercent(), or SeekToChar()SetText() methodsaria-live="polite" and aria-atomic="false" for screen readersprefers-reduced-motion media query when enabled<Typewriter Speed="100" Autostart="true">
<p>A simple typewriter with <strong>HTML support</strong>.</p>
</Typewriter>
<Typewriter @ref="_typewriter" Speed="60" Autostart="false">
<p>Click the buttons to control the animation.</p>
</Typewriter>
<button @onclick="() => _typewriter?.Start()">Start</button>
<button @onclick="() => _typewriter?.Pause()">Pause</button>
<button @onclick="() => _typewriter?.Resume()">Resume</button>
<button @onclick="() => _typewriter?.Complete()">Complete</button>
@code {
private Typewriter? _typewriter;
}
<Typewriter Speed="80" OnProgress="HandleProgress">
<p>Content to animate...</p>
</Typewriter>
<p>Progress: @_progress%</p>
@code {
private double _progress = 0;
private void HandleProgress(TypewriterProgressEventArgs args)
{
_progress = args.Percent;
}
}
<Typewriter Dir="rtl" Speed="80">
<p>يدعم المكوّن <strong>النصوص العربية</strong> مع الحفاظ على الاتجاه الصحيح.</p>
</Typewriter>
<Typewriter Speed="60">
<div>
<h2>Rich Content</h2>
<p>Supports <strong>bold</strong>, <em>italic</em>, and <a href="#">links</a>.</p>
<ul>
<li>Lists with <code>inline code</code></li>
<li>Nested <strong>formatting</strong></li>
</ul>
</div>
</Typewriter>
| Parameter | Type | Default | Description |
|---|---|---|---|
ChildContent | RenderFragment? | null | Content to animate. Supports any HTML markup. |
Speed | int | 100 | Typing speed in characters per second. |
MinDuration | int | 100 | Minimum animation duration in milliseconds. |
MaxDuration | int | 30000 | Maximum animation duration in milliseconds. |
Autostart | bool | true | Auto-start animation on load. Set to false for manual control. |
Dir | string | "ltr" | Text direction: "ltr" or "rtl". |
RespectMotionPreference | bool | false | Respect prefers-reduced-motion media query. |
AriaLabel | string? | null | ARIA label for the container region. |
OnStart | EventCallback | — | Fired when animation starts. |
OnPause | EventCallback | — | Fired when animation pauses. |
OnResume | EventCallback | — | Fired when animation resumes. |
OnComplete | EventCallback | — | Fired when animation completes. |
OnReset | EventCallback | — | Fired when component resets. |
OnProgress | EventCallback<TypewriterProgressEventArgs> | — | Fired every 10 characters with progress info. |
OnSeek | EventCallback<TypewriterSeekEventArgs> | — | Fired when seeking to a new position. |
| Method | Description |
|---|---|
Task Start() | Start the animation from the beginning. |
Task Pause() | Pause the current animation. |
Task Resume() | Resume a paused animation. |
Task Complete() | Complete the animation instantly. |
Task Reset() | Reset the component, clearing content and state. |
Task SetText(RenderFragment content) | Replace content with a new RenderFragment and reset. |
Task SetText(string html) | Replace content with an HTML string and reset. |
Task Seek(double position) | Seek to a position (0.0 to 1.0). Pauses if animating. |
Task SeekToPercent(double percent) | Seek to a percentage (0 to 100). |
Task SeekToChar(int charIndex) | Seek to a specific character index. |
TypewriterProgressInfo GetProgress() | Get current progress information. |
| Property | Type | Description |
|---|---|---|
IsRunning | bool | Whether the component is currently animating. |
IsPaused | bool | Whether the component is currently paused. |
Provides progress information:
Current (int) — Characters animated so farTotal (int) — Total characters to animatePercent (double) — Percentage complete (0-100)Provides seek information:
Position (double) — Normalized position (0.0 to 1.0)TargetChar (int) — Character index seeked toTotalChars (int) — Total number of charactersPercent (double) — Percentage complete (0-100)WasRunning (bool) — Whether animation was running before seekCanResume (bool) — Whether animation can be resumedAtStart (bool) — Whether seek landed at startAtEnd (bool) — Whether seek landed at endReturned by GetProgress():
Current (int) — Current character countTotal (int) — Total character countPercent (double) — Percentage complete (0-100)Position (double) — Normalized position (0.0 to 1.0)Jump to any position in the animation with full scrubbing support:
<Typewriter @ref="_typewriter" Speed="60" OnProgress="UpdatePosition">
<p>Content to animate with seek support...</p>
</Typewriter>
<label>
Position: @_position%
<input type="range" min="0" max="100" value="@_position"
@oninput="e => SeekToPosition(e)" />
</label>
<button @onclick="() => _typewriter?.Seek(0)">Start</button>
<button @onclick="() => _typewriter?.Seek(0.5)">50%</button>
<button @onclick="() => _typewriter?.Seek(1)">End</button>
@code {
private Typewriter? _typewriter;
private double _position = 0;
private async Task SeekToPosition(ChangeEventArgs e)
{
if (double.TryParse(e.Value?.ToString(), out var value))
{
_position = value;
await (_typewriter?.SeekToPercent(value) ?? Task.CompletedTask);
}
}
private void UpdatePosition(TypewriterProgressEventArgs args)
{
_position = args.Percent;
}
}
Update content programmatically at runtime:
<Typewriter @ref="_typewriter" Autostart="false">
@_content
</Typewriter>
<button @onclick="UpdateContent">Update Content</button>
@code {
private Typewriter? _typewriter;
private RenderFragment _content = builder =>
builder.AddMarkupContent(0, "<p>Initial content</p>");
private async Task UpdateContent()
{
await (_typewriter?.SetText("<p>New <strong>dynamic</strong> content!</p>")
?? Task.CompletedTask);
await (_typewriter?.Start() ?? Task.CompletedTask);
}
}
Reduced Motion Support
Respects user preferences for reduced motion:
<Typewriter RespectMotionPreference="true" Speed="100">
<p>This animation respects user motion preferences.</p>
</Typewriter>
ARIA Labels
Provide context for screen readers:
<Typewriter AriaLabel="Chat message being typed">
<p>Message content...</p>
</Typewriter>
Best Practices
Speed between 50-150 chars/sec for comfortable readingRespectMotionPreference for accessibility complianceAriaLabel when typewriter conveys essential informationChildContentThe component is optimized for trimming and Native AOT compilation:
dotnet publish -c Release \
-p:PublishTrimmed=true \
-p:TrimMode=link \
-p:RunAOTCompilation=true
Notes:
InvariantGlobalization in your project fileDOM Extraction — Uses JavaScript interop to extract rendered DOM structure, preserving all HTML tags and attributes.
Operation Queue — Converts DOM structure into an immutable array of operations (open tag, character, close tag) for efficient processing.
Animation Loop — Runs on background thread using Task.Run with proper cancellation token support for responsive UI.
Thread Safety — All UI updates use InvokeAsync to ensure thread-safe rendering and prevent race conditions.
[] for empty arrays, [..] for spread operationsis null or { Length: 0 }Lock type for thread-safe operationsStringBuilder with capacity and ImmutableArray.BuilderThe project includes comprehensive BUnit tests covering:
Run tests locally:
dotnet test
MIT