Blazor Component Created: 01 Feb 2026 Updated: 01 Feb 2026

Reusable Components and Shareable Pages in Blazor

One of the most powerful features of modern web frameworks is component reusability. Blazor takes this concept further by allowing developers to create both reusable UI components and shareable page components that can be used across multiple projects, whether they're running on Server or WebAssembly. In this comprehensive guide, we'll explore how to build a shared component library, understand the differences between reusable components and page components, and learn best practices for sharing code across Blazor applications.

Understanding the Component Types

Reusable Components

Reusable components are presentational building blocks that don't have routes. They are designed to be composed into larger views and can be used multiple times throughout an application. Think of them as LEGO blocks that you can combine to build complex UIs.

Key Characteristics:

  1. No @page directive
  2. Accept parameters via [Parameter] attribute
  3. Focus on presentation and interaction
  4. Can be used by both Server and WebAssembly projects
  5. Only require a project reference to be used

Page Components (Routed Pages)

Page components are routable views that respond to specific URLs. They represent entire pages or views in your application and are discovered by the Blazor Router.

Key Characteristics:

  1. Have @page directive with a route template
  2. Can define multiple routes using multiple @page directives
  3. Require AdditionalAssemblies configuration in Router when in external libraries
  4. Can accept route parameters
  5. Represent complete views/pages

Creating a Shared Component Library

Let's create a Razor Class Library that can be shared between multiple Blazor projects.

Project Structure

Solution
├── BlazorApp (Server)
├── BlazorApp.Client (WebAssembly)
└── SharedComponents (Razor Class Library)
├── Components/ # Reusable components (no @page)
│ ├── SharedButton.razor
│ ├── SharedCard.razor
│ ├── SharedAlert.razor
│ └── SharedModal.razor
└── Pages/ # Page components (with @page)
└── SharedComponentsDemo.razor

Creating the Razor Class Library

<!-- SharedComponents.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.2" />
</ItemGroup>
</Project>

Building Reusable Components

Example 1: SharedButton Component

A flexible button component with various styles, sizes, and icon support.

<!-- SharedComponents/Components/SharedButton.razor -->
<button class="btn @ButtonClass @SizeClass"
type="@Type"
disabled="@Disabled"
@onclick="OnClick">
@if (!string.IsNullOrEmpty(Icon))
{
<i class="bi bi-@Icon"></i>
}
@ChildContent
</button>

@code {
[Parameter]
public string ButtonStyle { get; set; } = "primary";

[Parameter]
public string Size { get; set; } = "medium";

[Parameter]
public string? Icon { get; set; }

[Parameter]
public bool Disabled { get; set; }

[Parameter]
public string Type { get; set; } = "button";

[Parameter]
public RenderFragment? ChildContent { get; set; }

[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }

private string ButtonClass => ButtonStyle switch
{
"primary" => "btn-primary",
"secondary" => "btn-secondary",
"success" => "btn-success",
"danger" => "btn-danger",
"warning" => "btn-warning",
"info" => "btn-info",
"light" => "btn-light",
"dark" => "btn-dark",
"outline-primary" => "btn-outline-primary",
"outline-secondary" => "btn-outline-secondary",
"link" => "btn-link",
_ => "btn-primary"
};

private string SizeClass => Size switch
{
"small" => "btn-sm",
"large" => "btn-lg",
_ => ""
};
}

Usage:

<SharedButton ButtonStyle="primary" Icon="heart" OnClick="@HandleClick">
Like
</SharedButton>

<SharedButton ButtonStyle="danger" Icon="trash" Size="small" Disabled="@isProcessing">
Delete
</SharedButton>

Example 2: SharedCard Component

A versatile card component with customizable header, body, and footer sections.

<!-- SharedComponents/Components/SharedCard.razor -->
<div class="card @AdditionalClasses" style="@Style">
@if (!string.IsNullOrEmpty(ImageUrl))
{
<img src="@ImageUrl" class="card-img-top" alt="@ImageAlt" />
}

@if (!string.IsNullOrEmpty(Title) || HeaderContent != null)
{
<div class="card-header @HeaderClass">
@if (HeaderContent != null)
{
@HeaderContent
}
else
{
<h5 class="card-title mb-0">@Title</h5>
}
</div>
}

<div class="card-body">
@if (!string.IsNullOrEmpty(Title) && HeaderContent == null && string.IsNullOrEmpty(ImageUrl))
{
<h5 class="card-title">@Title</h5>
}

@if (!string.IsNullOrEmpty(Subtitle))
{
<h6 class="card-subtitle mb-2 text-muted">@Subtitle</h6>
}

@ChildContent
</div>

@if (FooterContent != null)
{
<div class="card-footer @FooterClass">
@FooterContent
</div>
}
</div>

@code {
[Parameter]
public string? Title { get; set; }

[Parameter]
public string? Subtitle { get; set; }

[Parameter]
public string? ImageUrl { get; set; }

[Parameter]
public string? ImageAlt { get; set; }

[Parameter]
public RenderFragment? ChildContent { get; set; }

[Parameter]
public RenderFragment? HeaderContent { get; set; }

[Parameter]
public RenderFragment? FooterContent { get; set; }

[Parameter]
public string AdditionalClasses { get; set; } = string.Empty;

[Parameter]
public string HeaderClass { get; set; } = string.Empty;

[Parameter]
public string FooterClass { get; set; } = string.Empty;

[Parameter]
public string? Style { get; set; }
}

Usage Examples:

<!-- Simple Card -->
<SharedCard Title="Product Details" Subtitle="Premium Edition">
<p>This is a high-quality product with excellent features.</p>
<p><strong>Price:</strong> $299.99</p>
</SharedCard>

<!-- Card with Image -->
<SharedCard Title="Featured Product" ImageUrl="/images/product.jpg" ImageAlt="Product image">
<p>Limited time offer!</p>
</SharedCard>

<!-- Card with Custom Header and Footer -->
<SharedCard>
<HeaderContent>
<div class="d-flex justify-content-between align-items-center">
<h5>User Profile</h5>
<SharedBadge BadgeStyle="success">Active</SharedBadge>
</div>
</HeaderContent>
<ChildContent>
<p><strong>Name:</strong> John Doe</p>
<p><strong>Email:</strong> john@example.com</p>
</ChildContent>
<FooterContent>
<SharedButton ButtonStyle="primary" Icon="pencil">Edit Profile</SharedButton>
</FooterContent>
</SharedCard>

Example 3: SharedModal Component

A modal dialog component with programmable show/hide functionality.

<!-- SharedComponents/Components/SharedModal.razor -->
@if (IsVisible)
{
<div class="modal fade show d-block" tabindex="-1" role="dialog"
style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog @SizeClass @(IsCentered ? "modal-dialog-centered" : "")"
role="document">
<div class="modal-content">
@if (!string.IsNullOrEmpty(Title) || HeaderContent != null)
{
<div class="modal-header">
@if (HeaderContent != null)
{
@HeaderContent
}
else
{
<h5 class="modal-title">@Title</h5>
}
@if (ShowCloseButton)
{
<button type="button" class="btn-close"
@onclick="Close" aria-label="Close"></button>
}
</div>
}

<div class="modal-body">
@ChildContent
</div>

@if (FooterContent != null)
{
<div class="modal-footer">
@FooterContent
</div>
}
</div>
</div>
</div>
}

@code {
[Parameter]
public string? Title { get; set; }

[Parameter]
public string Size { get; set; } = "medium";

[Parameter]
public bool IsCentered { get; set; } = true;

[Parameter]
public bool ShowCloseButton { get; set; } = true;

[Parameter]
public RenderFragment? ChildContent { get; set; }

[Parameter]
public RenderFragment? HeaderContent { get; set; }

[Parameter]
public RenderFragment? FooterContent { get; set; }

[Parameter]
public EventCallback OnClosed { get; set; }

private bool IsVisible { get; set; }

private string SizeClass => Size switch
{
"small" => "modal-sm",
"large" => "modal-lg",
"xlarge" => "modal-xl",
_ => ""
};

public void Show()
{
IsVisible = true;
StateHasChanged();
}

public async Task Close()
{
IsVisible = false;
await OnClosed.InvokeAsync();
StateHasChanged();
}
}

Usage:

<SharedButton ButtonStyle="primary" OnClick="@ShowConfirmation">
Delete Account
</SharedButton>

<SharedModal @ref="confirmModal" Title="Confirm Deletion" Size="medium">
<ChildContent>
<SharedAlert AlertType="danger" Icon="exclamation-triangle">
<strong>Warning:</strong> This action cannot be undone!
</SharedAlert>
<p>Are you sure you want to delete your account?</p>
</ChildContent>
<FooterContent>
<SharedButton ButtonStyle="secondary" OnClick="@(() => confirmModal?.Close())">
Cancel
</SharedButton>
<SharedButton ButtonStyle="danger" OnClick="@ConfirmDeletion">
Yes, Delete My Account
</SharedButton>
</FooterContent>
</SharedModal>

@code {
private SharedModal? confirmModal;
private void ShowConfirmation() => confirmModal?.Show();
private async Task ConfirmDeletion()
{
await DeleteAccountAsync();
await confirmModal!.Close();
}
}

Creating Page Components with Routes

Page components live in shared libraries but behave like regular pages with routes.

Example: SharedComponentsDemo Page

<!-- SharedComponents/Pages/SharedComponentsDemo.razor -->
@page "/shared-demo"

<PageTitle>Shared Components Demo Page</PageTitle>

<div class="container mt-4">
<h1>Shared Components Demo Page</h1>
<SharedAlert AlertType="info" Icon="info-circle" Title="Routed Page Example">
This page is located in the <strong>SharedComponents</strong> library and uses
the <code>@("@page")</code> directive. It demonstrates that pages can also be
shared between projects!
</SharedAlert>

<div class="row mt-4">
<div class="col-md-6">
<SharedCard Title="Interactive Counter" Subtitle="Using shared components">
<div class="text-center">
<h2 class="display-4">@count</h2>
<div class="d-flex justify-content-center gap-2 mt-3">
<SharedButton ButtonStyle="success" Icon="plus" OnClick="@Increment">
Increment
</SharedButton>
<SharedButton ButtonStyle="danger" Icon="dash" OnClick="@Decrement">
Decrement
</SharedButton>
<SharedButton ButtonStyle="warning" Icon="arrow-clockwise" OnClick="@Reset">
Reset
</SharedButton>
</div>
</div>
</SharedCard>
</div>

<div class="col-md-6">
<SharedCard Title="Current Status">
<HeaderContent>
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">Current Status</h5>
<SharedBadge BadgeStyle="@GetCountBadgeStyle()" IsPill="true">
@GetCountStatus()
</SharedBadge>
</div>
</HeaderContent>
<ChildContent>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<strong>Count Value:</strong> @count
</li>
<li class="list-group-item">
<strong>Page Location:</strong> SharedComponents Library
</li>
<li class="list-group-item">
<strong>Render Mode:</strong> @GetRenderMode()
</li>
</ul>
</ChildContent>
</SharedCard>
</div>
</div>
</div>

@code {
private int count = 0;

private void Increment() => count++;
private void Decrement() => count--;
private void Reset() => count = 0;

private string GetCountBadgeStyle() => count switch
{
> 0 => "success",
< 0 => "danger",
_ => "secondary"
};

private string GetCountStatus() => count switch
{
> 0 => "Positive",
< 0 => "Negative",
_ => "Zero"
};

private string GetRenderMode()
{
return OperatingSystem.IsBrowser() ? "WebAssembly" : "Server";
}
}

Configuring Projects to Use Shared Components

Step 1: Add Project Reference

Add the SharedComponents project reference to both your Server and WebAssembly projects.

BlazorApp.csproj (Server):

<ItemGroup>
<ProjectReference Include="..\..\SharedComponents\SharedComponents.csproj" />
</ItemGroup>

BlazorApp.Client.csproj (WebAssembly):

<ItemGroup>
<ProjectReference Include="..\..\SharedComponents\SharedComponents.csproj" />
</ItemGroup>

Step 2: Import Namespace

Add the namespace to your _Imports.razor files in both projects.

<!-- BlazorApp/Components/_Imports.razor -->
@using SharedComponents.Components

<!-- BlazorApp.Client/_Imports.razor -->
@using SharedComponents.Components

Step 3: Configure Router for Page Components

This is the critical step that many developers miss. If your shared library contains page components (with @page directive), you must tell the Router where to find them.

<!-- BlazorApp/Components/Routes.razor -->
<Router AppAssembly="typeof(Program).Assembly"
AdditionalAssemblies="new[] {
typeof(Client._Imports).Assembly,
typeof(SharedComponents.Components.SharedButton).Assembly
}"
NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

Understanding AdditionalAssemblies

Why is AdditionalAssemblies Needed?

The Blazor Router automatically discovers pages (components with @page directive) only in the main assembly. When you have routed pages in external libraries, the Router needs to be explicitly told about those assemblies.

Component vs. Page: The Key Difference

┌─────────────────────────────────────────────────────────────┐
│ COMPONENT TYPE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Reusable Component │ Page Component │
│ (No @page directive) │ (Has @page directive) │
│ │ │
│ ✓ Project reference only │ ✓ Project reference │
│ ✗ AdditionalAssemblies NOT │ ✓ AdditionalAssemblies │
│ needed │ REQUIRED │
│ │ │
│ Example: │ Example: │
│ SharedButton.razor │ SharedDemo.razor │
│ <button>...</button> │ @page "/shared-demo" │
│ │ <h1>...</h1> │
│ │ │
│ Usage: │ Usage: │
│ <SharedButton>Click</SharedButton> NavigateTo("/shared-demo") │
│ │ │
└─────────────────────────────────────────────────────────────┘

What Happens Without AdditionalAssemblies?

<!-- Missing AdditionalAssemblies -->
<Router AppAssembly="typeof(Program).Assembly">

Result:

  1. ✅ Main project pages work (/counter, /weather)
  2. ✅ Reusable components work (<SharedButton>)
  3. ❌ Shared library pages return 404 Not Found (/shared-demo)

Correct Configuration

<Router AppAssembly="typeof(Program).Assembly"
AdditionalAssemblies="new[] {
typeof(Client._Imports).Assembly,
typeof(SharedComponents.Components.SharedButton).Assembly
}">

Result:

  1. ✅ Main project pages work
  2. ✅ Reusable components work
  3. ✅ Shared library pages work

Real-World Usage Scenarios

Scenario 1: E-Commerce Application

Solution
├── ECommerce.Web (Server)
├── ECommerce.Client (WebAssembly)
└── ECommerce.Shared
├── Components/
│ ├── ProductCard.razor # Reusable
│ ├── AddToCartButton.razor # Reusable
│ ├── PriceDisplay.razor # Reusable
│ └── RatingStars.razor # Reusable
└── Pages/
├── ProductDetails.razor # @page "/product/{id}"
└── SearchResults.razor # @page "/search"

ProductCard Component:

<!-- Reusable component - no @page -->
<SharedCard ImageUrl="@Product.ImageUrl" ImageAlt="@Product.Name">
<HeaderContent>
<div class="d-flex justify-content-between">
<h5>@Product.Name</h5>
<SharedBadge BadgeStyle="@GetStockBadge()">
@Product.StockStatus
</SharedBadge>
</div>
</HeaderContent>
<ChildContent>
<p>@Product.Description</p>
<PriceDisplay Price="@Product.Price" DiscountPrice="@Product.DiscountPrice" />
<RatingStars Rating="@Product.Rating" />
</ChildContent>
<FooterContent>
<AddToCartButton ProductId="@Product.Id" />
</FooterContent>
</SharedCard>

@code {
[Parameter, EditorRequired]
public Product Product { get; set; } = null!;
private string GetStockBadge() => Product.InStock ? "success" : "danger";
}

ProductDetails Page:

<!-- Page component - has @page -->
@page "/product/{ProductId:int}"
@inject IProductService ProductService

<PageTitle>@product?.Name</PageTitle>

@if (isLoading)
{
<SharedSpinner SpinnerType="border" Alignment="center" />
}
else if (product != null)
{
<div class="row">
<div class="col-md-6">
<img src="@product.ImageUrl" class="img-fluid" alt="@product.Name" />
</div>
<div class="col-md-6">
<h1>@product.Name</h1>
<RatingStars Rating="@product.Rating" ReviewCount="@product.ReviewCount" />
<PriceDisplay Price="@product.Price" DiscountPrice="@product.DiscountPrice" />
<p class="mt-3">@product.DetailedDescription</p>
<AddToCartButton ProductId="@product.Id" ShowQuantitySelector="true" />
</div>
</div>
}

@code {
[Parameter]
public int ProductId { get; set; }
private Product? product;
private bool isLoading = true;
protected override async Task OnParametersSetAsync()
{
isLoading = true;
product = await ProductService.GetProductByIdAsync(ProductId);
isLoading = false;
}
}


Share this lesson: