A high-performance Blazor AutoComplete and typeahead component with AI semantic search, 8 display modes, and virtualization for 100K+ items. Fully AOT compatible for .NET 8, 9, and 10.
If you've ever needed a Blazor autocomplete, typeahead, or autosuggest component that handles real-world datasets without choking, you know the pain. Most components work fine with 100 items. Load 10,000 products? The browser tab freezes. Add semantic search requirements? Now you're building custom infrastructure.
This article walks through a Blazor AutoComplete component I built after implementing search-as-you-type functionality one too many times. It handles 100,000+ items at 60fps, includes AI-powered semantic search, and ships under 15KB gzipped. Call it autocomplete, typeahead, autosuggest. Whatever. Same problem, same solution.
TL;DR
Built a Blazor AutoComplete component that:
- Handles 100,000+ items at 60fps (seriously, try scrolling)
- AI-powered semantic search - type "automobile" and find "car"
- 8 built-in display modes - stop rewriting the same ItemTemplate
- < 15KB gzipped - smaller than most favicons these days
- AOT compatible - core package is fully trimmable (AI packages have SK dependency)
- 5 vector database providers - PostgreSQL, Azure AI Search, Pinecone, Qdrant, CosmosDB
Works on: .NET 8, 9, and 10 | Rendering modes: WebAssembly, Server, Auto
dotnet add package EasyAppDev.Blazor.AutoComplete
The Problem: Why Most Autocomplete Components Fall Short
Every Blazor project eventually needs a typeahead or autosuggest input. You start with the basics: an input field, an @oninput handler, and a foreach loop. It works for your demo with 50 items:
<input @bind="searchText" @oninput="Filter" />
@foreach (var item in filteredItems)
{
<div @onclick="() => Select(item)">@item.Name</div>
}
Then reality hits. Requirements creep in one by one:
- "Can it search by description too?" - Now you need multi-field filtering
- "We need it to look like our design system" - Custom templates for every project
- "It's slow with 5,000 products" - Virtualization becomes mandatory
- "Users keep misspelling things" - Fuzzy matching or they can't find anything
- "Can it find related items, like 'vehicle' matching 'car'?" - Semantic search territory
Six months later, you've got 800 lines of autocomplete code, zero test coverage, and a new developer asking "what does this do?" The component that started as 20 lines now handles edge cases you forgot existed.
I've been there. Multiple times across different projects. This component exists because there had to be a better way.
Getting Started: Add Blazor AutoComplete in 30 Seconds
The fastest way to add a production-ready typeahead to your Blazor app. Two lines of setup, then drop the component into any page. The component handles keyboard navigation, ARIA accessibility attributes, input debouncing, and focus management automatically. No configuration required.
Step 1: Register the service in Program.cs
// Program.cs
builder.Services.AddAutoComplete();
Step 2: Add the component to your Razor page
The generic TItem parameter works with any class. The TextField expression tells the component which property to display and search against. Two-way binding with @bind-Value gives you the selected item.
@using EasyAppDev.Blazor.AutoComplete
<AutoComplete TItem="Product"
Items="@products"
TextField="@(p => p.Name)"
@bind-Value="@selectedProduct"
Placeholder="Search products..." />
Done. You get a combobox with keyboard navigation (Arrow keys, Enter, Escape, Home, End), screen reader support, and animations. No JavaScript.
Search Multiple Fields: Name, Description, SKU in One Query
The most requested feature for any autosuggest component. Real users don't know which field contains the data they're looking for. They just type "ergonomic" and expect to find everything related, whether it's in the name, description, category, or SKU.
The SearchFields parameter accepts a lambda returning an array of strings. The component searches all fields with OR logic, so a match in any field returns the item.
<AutoComplete TItem="Product"
Items="@products"
TextField="@(p => p.Name)"
SearchFields="@(p => new[] { p.Name, p.Description, p.Category, p.SKU })"
FilterStrategy="FilterStrategy.Contains"
Placeholder="Search everything..." />
What happens when you type "ergonomic":
- Finds "Ergonomic Chair" (name match)
- Finds "Wireless Mouse" with description "Ergonomic wireless mouse..."
- Finds anything tagged "ergonomic" in category
All OR logic. No custom filtering code needed.
8 Built-in Display Modes: Stop Writing Custom Templates
I looked at my old projects. Dozens of autocomplete implementations. Most had nearly identical ItemTemplate markup: title on top, description below, maybe a badge. Writing the same template over and over wastes time and introduces inconsistency.
Now there are 8 built-in display modes that cover 90% of use cases. Pick a mode, map your properties, and move on. Custom templates are still available when you need complete control.
<!-- Two-line layout: bold title + muted description -->
<AutoComplete DisplayMode="ItemDisplayMode.TitleWithDescription"
DescriptionField="@(p => p.Category)" ... />
<!-- Title with right-aligned price badge -->
<AutoComplete DisplayMode="ItemDisplayMode.TitleWithBadge"
BadgeField="@(p => $"${p.Price:F2}")" ... />
<!-- Icon/emoji on left + title + description -->
<AutoComplete DisplayMode="ItemDisplayMode.IconTitleDescription"
IconField="@(p => p.Emoji)"
DescriptionField="@(p => p.Category)" ... />
<!-- Full card layout with all fields -->
<AutoComplete DisplayMode="ItemDisplayMode.Card"
IconField="@(p => p.Emoji)"
SubtitleField="@(p => p.Category)"
DescriptionField="@(p => p.Description)"
BadgeField="@(p => $"${p.Price:F2}")" ... />
Available modes: Simple, TitleWithDescription, TitleWithBadge, TitleDescriptionBadge, IconWithTitle, IconTitleDescription, Card, Custom
Card mode packs everything into one row: thumbnail, title, subtitle, description, badge. Looks like what you'd see in Google or Amazon's search dropdowns.
Filter Strategies: From Fast Prefix Matching to Typo Tolerance
Different use cases need different filtering algorithms. A product SKU lookup needs exact prefix matching for speed. A customer-facing search needs typo tolerance because users can't spell. The component includes four built-in strategies.
| Strategy | Best Use Case | Performance (100K items) |
|---|---|---|
StartsWith |
SKU lookup, known prefixes | ~3ms |
Contains |
General search, substring matching | ~5ms |
Fuzzy |
User-facing search with typo tolerance | ~70ms |
Custom |
Your own algorithm | Depends on implementation |
Enabling fuzzy search for typo tolerance:
Users type "laptpo" and find "Laptop." They type "chiar" and find "Chair." Fuzzy matching uses Levenshtein distance to handle common typos without requiring exact spelling.
<AutoComplete FilterStrategy="FilterStrategy.Fuzzy"
... />
Look, fuzzy search won't find "computer" when someone types "laptop." That's not what Levenshtein does. But transposed letters, missing characters, common typos? Handles those fine. For actual concept matching, you need the AI stuff below.
Virtualization for 100K+ Items at 60fps
Most typeahead components die here. Load 10,000 items, browser struggles. Load 100,000, tab freezes. The DOM can't handle that many elements at once.
Virtualization solves this by only rendering items currently visible in the viewport. Scroll down, and items render on demand. The component maintains a scroll container with calculated height, creating the illusion of a complete list while only materializing visible rows.
<AutoComplete TItem="Product"
Items="@largeDataset"
Virtualize="true"
VirtualizationThreshold="100"
ItemHeight="40"
... />
What this gives you:
- Only visible items hit the DOM
- 60fps scrolling, tested with 100K items
- Kicks in automatically past threshold
- Works with grouping headers too
The ItemHeight parameter should match your CSS. Accurate height calculation ensures smooth scrolling without visual jumps. I tested this with 100K products on an M1 MacBook Air. Scrolling stayed smooth, memory stayed reasonable, browser never complained.
AI Semantic Search: Find "car" When Users Type "automobile"
Traditional text matching breaks down when users don't know your terminology. They search for what they mean, not what you labeled it.
With semantic search:
- Type "automobile" → find "Toyota Camry"
- Type "mobile apps" → find "React Native Tutorial"
- Type "password security" → find "OAuth Implementation Guide"
Embedding models convert text into vectors. Similar concepts end up near each other in vector space. "Automobile" clusters with "car" and "sedan" even though they share zero letters.
Setting Up AI-Powered Autosuggest
dotnet add package EasyAppDev.Blazor.AutoComplete.AI
For OpenAI embeddings:
// Program.cs
builder.Services.AddAutoCompleteSemanticSearch(
apiKey: "sk-...",
model: "text-embedding-3-small"
);
For Azure OpenAI:
builder.Services.AddAutoCompleteSemanticSearchWithAzure(
endpoint: "https://my-resource.openai.azure.com/",
apiKey: "...",
deploymentName: "text-embedding-ada-002"
);
Using the semantic autocomplete component:
<SemanticAutoComplete TItem="Document"
Items="@documents"
SearchFields="@(d => new[] { d.Title, d.Description, d.Tags })"
SimilarityThreshold="0.15"
@bind-Value="@selectedDoc"
Placeholder="Search by meaning..." />
The SimilarityThreshold controls how closely items must match the query. Lower values (0.1) return more results with looser matching. Higher values (0.3) require stronger semantic similarity.
Managing Embedding Costs with Dual Caching
Embedding API calls cost money. Each search query requires one API call to embed the query. Each item needs embedding to enable similarity comparison. Without caching, costs spiral quickly.
The component uses dual caching to minimize API calls:
| Cache | Default TTL | Max Size | Purpose |
|---|---|---|---|
| Item Cache | 1 hour | 10,000 items | Your data embeddings |
| Query Cache | 15 minutes | 1,000 queries | User search queries |
Pre-warming for instant results:
<SemanticAutoComplete PreWarmCache="true" ... />
Pre-warming generates all embeddings on init. Users get instant results because everything's cached before they type. One-time hit, then it's fast.
How it stays fast:
- SIMD cosine similarity (
System.Numerics.Tensors), 3-5x faster than naive loops - LRU eviction when caches fill up
- Background cleanup purges expired entries every 5 min
Vector Database Providers: Production-Grade Semantic Search
In-memory caching works perfectly for development and small datasets. Production deployments with millions of products need persistent vector storage with approximate nearest neighbor (ANN) indexing.
Five providers integrate directly with the component:
| Provider | Package | Best For |
|---|---|---|
| PostgreSQL/pgvector | .AI.PostgreSql |
Self-hosted, existing Postgres infrastructure |
| Azure AI Search | .AI.AzureSearch |
Enterprise, hybrid search (semantic + keyword) |
| Pinecone | .AI.Pinecone |
Serverless, automatic scaling |
| Qdrant | .AI.Qdrant |
Open-source, self-hosted with advanced filtering |
| Azure CosmosDB | .AI.CosmosDb |
Global distribution, multi-model |
PostgreSQL with pgvector Example
PostgreSQL with the pgvector extension is the most accessible option for teams already running Postgres. No new infrastructure required. Just enable the extension and create a vector column.
dotnet add package EasyAppDev.Blazor.AutoComplete.AI.PostgreSql
Configuration in appsettings.json:
{
"VectorSearch": {
"PostgreSQL": {
"ConnectionString": "Host=localhost;Database=myapp;...",
"CollectionName": "products",
"EmbeddingDimensions": 1536
}
},
"OpenAI": {
"ApiKey": "sk-..."
}
}
Service registration:
builder.Services.AddAutoCompletePostgres<Product>(
builder.Configuration,
textSelector: p => $"{p.Name} {p.Description}",
idSelector: p => p.Id.ToString());
builder.Services.AddAutoCompleteVectorSearch<Product>(builder.Configuration);
Indexing your product catalog:
Before semantic search works, your data needs embedding and storage in the vector database. The IVectorIndexer<T> service handles batch indexing with automatic embedding generation.
public class ProductService
{
private readonly IVectorIndexer<Product> _indexer;
public async Task IndexProducts(IEnumerable<Product> products)
{
await _indexer.EnsureCollectionExistsAsync();
await _indexer.IndexAsync(products);
}
}
Run indexing once when your data changes. Queries hit the vector database directly. No runtime embedding of your catalog needed.
OData Integration: Server-Side Typeahead Filtering
For applications with server-side data, the OData package generates $filter queries automatically. The component sends search requests to your API endpoint rather than filtering locally.
dotnet add package EasyAppDev.Blazor.AutoComplete.OData
Configuring the OData data source:
var options = new ODataOptions
{
EndpointUrl = "https://api.example.com/odata/Products",
FilterStrategy = ODataFilterStrategy.Contains,
Top = 20,
CaseInsensitive = true
};
_odataSource = new ODataDataSource<Product>(Http, options,
searchFieldNames: new[] { "Name", "Description", "Category" });
Generated query when user types "laptop":
GET /odata/Products?$filter=(contains(tolower(Name),'laptop') or contains(tolower(Description),'laptop') or contains(tolower(Category),'laptop'))&$top=20
Debouncing, request cancellation, loading states: handled. Your API just gets clean OData queries.
Supports both OData v3 and v4 protocols. v3 uses substringof() instead of contains() for substring matching.
Theming: Material, Fluent, Bootstrap, or Custom
Four presets, each with light/dark variants:
<AutoComplete ThemePreset="ThemePreset.Material" ... />
<AutoComplete ThemePreset="ThemePreset.Fluent" ... />
<AutoComplete ThemePreset="ThemePreset.Modern" ... />
<AutoComplete ThemePreset="ThemePreset.Bootstrap" ... />
Bootstrap colors:
All nine Bootstrap theme colors. Hover states, focus rings, selection styles generated automatically.
<AutoComplete BootstrapTheme="BootstrapTheme.Primary" ... />
<AutoComplete BootstrapTheme="BootstrapTheme.Success" ... />
<AutoComplete BootstrapTheme="BootstrapTheme.Danger" ... />
Dark mode:
Theme.Auto follows OS preference. CSS media queries, no JS.
<AutoComplete Theme="Theme.Auto" ... /> <!-- Follows OS preference -->
<AutoComplete Theme="Theme.Dark" ... /> <!-- Force dark mode -->
Custom overrides:
Presets not your thing? Override individual properties:
<AutoComplete PrimaryColor="#FF6B6B"
BorderRadius="8px"
FontFamily="Inter, sans-serif"
DropdownShadow="0 4px 12px rgba(0,0,0,0.15)"
... />
Grouping Results by Category
Group items by any property to help users scan large result sets. Each group displays a header, and items appear nested beneath their category.
<AutoComplete TItem="Product"
Items="@products"
TextField="@(p => p.Name)"
GroupBy="@(p => p.Category)">
<GroupTemplate Context="group">
<div class="group-header">
<strong>@group.Key</strong>
<span class="badge">@group.Count()</span>
</div>
</GroupTemplate>
</AutoComplete>
Works with virtualization. Group headers render on scroll like everything else. No perf hit.
Full AOT and Trimming Compatibility
One constraint shaped the whole architecture: no reflection at runtime.
The normal approach to property access in generic components (TextField.Compile()) uses reflection internally. AOT hates that. Trimming hates that. Your deployed app crashes because the runtime can't find types that got trimmed.
So instead: source generators create typed accessors at build time.
Important caveat: This applies to the core EasyAppDev.Blazor.AutoComplete package. The AI packages depend on Semantic Kernel, which isn't trimmable yet. If you need AOT/trimming and semantic search, you'll need to wait for SK to catch up or use the vector database providers with a separate indexing service.
// You write this in your Razor component
TextField="@(p => p.Name)"
// Source generator creates this at compile time
public static string GetName(Product p) => p.Name;
Zero runtime cost. Full AOT compatibility. Works correctly with PublishAot=true and aggressive trimming.
The generators also catch invalid expressions at compile time:
-
EBDAC001- Invalid TextField expression -
EBDAC002- Invalid ValueField expression -
EBDAC003- Unsupported expression pattern
Build-time errors beat runtime surprises every time.
Accessibility: WCAG 2.1 AA
ARIA 1.2 Combobox pattern. Screen readers work. Keyboard-only users can navigate everything.
| Key | Action |
|---|---|
| Arrow Down | Open dropdown / Move to next item |
| Arrow Up | Move to previous item |
| Enter | Select highlighted item |
| Escape | Close dropdown |
| Home | Jump to first item |
| End | Jump to last item |
ARIA attributes you don't have to think about:
-
role="combobox"on input -
role="listbox"on dropdown -
role="option"on each item -
aria-activedescendantfor focus tracking -
aria-expanded,aria-selected,aria-busy
Also:
- High contrast mode support (
prefers-contrast: high) - Reduced motion (
prefers-reduced-motion: reduce) - RTL via
RightToLeft="true" - Works with
EditFormvalidation
Performance Benchmarks
Tested on M1 MacBook Air with .NET 9:
| Metric | Target | Actual |
|---|---|---|
| Bundle size (gzipped) | < 15KB | 12KB |
| Filter 100K items (StartsWith) | < 100ms | 3ms |
| Filter 100K items (Fuzzy) | < 100ms | 72ms |
| First render | < 50ms | 35ms |
| Virtualized scroll | 60fps | 60fps |
| SIMD cosine similarity (1536-dim) | N/A | 3-5x faster than naive |
Why's it fast? No unnecessary re-renders. Efficient DOM updates. Debounced input. Virtualization that actually works instead of just being a checkbox feature.
Fluent Configuration API
Setting 20 parameters inline gets ugly. Builder pattern cleans it up:
var config = AutoCompleteConfig<Product>.Create()
.WithItems(products)
.WithTextField(p => p.Name)
.WithSearchFields(p => new[] { p.Name, p.Description, p.Category })
.WithDisplayMode(ItemDisplayMode.TitleWithDescription)
.WithTitleAndDescription(p => p.Description)
.WithFilterStrategy(FilterStrategy.Contains)
.WithTheme(Theme.Auto)
.WithBootstrapTheme(BootstrapTheme.Primary)
.WithVirtualization(threshold: 1000, itemHeight: 45)
.WithGrouping(p => p.Category)
.WithDebounce(300)
.Build();
<AutoComplete TItem="Product" Config="@config" @bind-Value="@selected" />
Every parameter has a builder method. Nothing's left out.
When to Use What: Quick Reference
| Scenario | Recommendation |
|---|---|
| Small dataset (< 1K items) | Basic component with StartsWith filter |
| Medium dataset (1K-10K) | Enable virtualization |
| Large dataset (10K-100K) | Virtualization + StartsWith for speed |
| Users misspell often | FilterStrategy.Fuzzy |
| Need concept matching | AI package with embedding cache |
| Production AI (> 10K items) | Vector database provider |
| Server-side data | OData package |
| Multi-tenant shared data | Vector provider with collection per tenant |
Troubleshooting Common Issues
Dropdown not opening?
- Check
MinSearchLengthparameter (default: 1 character) - Verify
Itemscollection orDataSourceisn't null - Ensure the component has focus
Filtering returns no results?
- Confirm
TextFieldlambda returns a non-null string - Try
FilterStrategy.Containsinstead ofStartsWith - Check for leading/trailing whitespace in your data
Virtualization scrolling is jumpy?
- Set
ItemHeightto match your actual CSS item height - Verify item count exceeds
VirtualizationThreshold - Ensure all items have consistent heights
Semantic search returns nothing?
- Lower
SimilarityThreshold(try 0.1 or 0.12) - Check
MinSearchLengthfor AI component (default: 3) - Verify API key in browser console network tab
- Confirm items were pre-warmed or embedded
Installation Summary
# Core autocomplete/typeahead component
dotnet add package EasyAppDev.Blazor.AutoComplete
# OData server-side filtering
dotnet add package EasyAppDev.Blazor.AutoComplete.OData
# AI semantic search
dotnet add package EasyAppDev.Blazor.AutoComplete.AI
# Vector database providers (pick one based on your infrastructure)
dotnet add package EasyAppDev.Blazor.AutoComplete.AI.PostgreSql
dotnet add package EasyAppDev.Blazor.AutoComplete.AI.AzureSearch
dotnet add package EasyAppDev.Blazor.AutoComplete.AI.Pinecone
dotnet add package EasyAppDev.Blazor.AutoComplete.AI.Qdrant
dotnet add package EasyAppDev.Blazor.AutoComplete.AI.CosmosDb
Add CSS to your layout:
<link href="_content/EasyAppDev.Blazor.AutoComplete/styles/autocomplete.base.css" rel="stylesheet" />
Register services in Program.cs:
builder.Services.AddAutoComplete();
Frequently Asked Questions
Why choose this over MudBlazor or Radzen autocomplete?
MudBlazor and Radzen are great component libraries. Use them if you need a full suite of UI controls. But their autocompletes weren't built for 100K+ items or semantic search. This component was. Different tools, different problems.
Does it work with Blazor Server and WebAssembly?
Yeah, all of them. WebAssembly, Server, Auto. Same component, same code.
What's the bundle size impact?
Core component: 12KB gzipped. AI package: ~25KB extra. Vector providers: 5-10KB each.
Can I use my own embedding provider?
Anything that implements Microsoft.Extensions.AI.IEmbeddingGenerator. OpenAI, Azure OpenAI, Ollama, whatever. Register it in DI and you're done.
Is this production-ready?
250+ tests, 72% coverage, runs on .NET 8/9/10. Core package is AOT/trim friendly. AI packages pull in Semantic Kernel which isn't trimmable. If that matters, stick to the core component or run AI stuff server-side. I use it in production. MIT licensed, no warranties, but it's not a weekend hack.
Summary
So what's the point? Most autocomplete components work fine until you throw real data at them. This one doesn't choke on 100K items. It understands what users mean, not just what they type. And you don't have to write the same ItemTemplate for every project.
Works on .NET 8, 9, 10. AOT compatible. No reflection tricks that blow up in production.
What's Next
Open source on GitHub. Right now I'm working on Elasticsearch and Milvus providers, hybrid search (semantic + keyword combined), and getting test coverage above 90%. Eventually want CI benchmarks so regressions get caught automatically.
Links:
Got tired of building the same autocomplete over and over. Now it's a package. MIT licensed. PRs welcome.
About the Author
I'm Mashrul Haque, a Systems Architect with over 15 years of experience building enterprise applications with .NET, Blazor, ASP.NET Core, and SQL Server. I specialize in Azure cloud architecture, AI integration, and performance optimization.
When production catches fire at 2 AM, I'm the one they call.
- LinkedIn: Connect with me
- GitHub: mashrulhaque
- Twitter/X: @mashrulthunder
Follow me here on dev.to for more .NET and Blazor content
Top comments (0)