Developer’s Guide to Creating a Custom Umbraco 17 Backoffice Dashboard
Published by Debasish Gracias on 20 March 2026
Introduction
With the release of Umbraco 14 and later, the backoffice architecture underwent a major transformation. The legacy AngularJS-based UI was removed and replaced with a modern extension system built using Web Components, Lit, manifests, and extension bundles.
This new architecture provides a more modular and scalable way for developers to extend the Umbraco backoffice. Instead of building dashboards with AngularJS controllers and views, developers now create extensions that are registered through manifests and loaded as part of an extension bundle.
In this guide, we will walk through how to create a custom backoffice dashboard in Umbraco 17 using the official extension template.
Goal
The dashboard needs the following:
Upload a CSV file of members
Process the file through a backend API
Display how many records were processed
Display success and error notifications
By the end of this article, you will understand:
How the Umbraco extension architecture works
How to create an extension project using the extension template
How to register dashboards using manifests
How to build dashboard UI using Lit Web Components
How to connect your dashboard to backend APIs
This guide is aimed at developers working with Umbraco 17 who want to build custom backoffice tools and dashboards using the new extension framework.
Prerequisites
.NET SDK version 9.0 or later
Node.js version 22 or later
Step 1: Install the Umbraco Extension Template
To install the Umbraco extension template, run the following command in your terminal:
dotnet new install Umbraco.Templates::17.2.1This command installs both the umbraco and umbraco-extension templates, which you can use to create new Umbraco and Umbraco extension projects. If a new Umbraco project has previously been created using dotnet new umbraco, the templates may already be installed.
Step 2: Create a New Umbraco Extension
dotnet new umbraco-extension –-version 17.2.1 -n MyProject.BackofficeExtensions -exThis creates the following structure:
MyProject.BackofficeExtensions
│
├─ Client
│ ├─ public
│ │ └─ umbraco-package.json
│ └─ src
│ ├─ dashboards
│ ├─ entrypoints
│ └─ bundle.manifests.ts
└─ vite.config.tsAdd this project to your solution and reference it from your Umbraco web project.
Step 3: Install Dependencies
Run the following command in the Client folder of your extension project:
npm installThis installs the build tooling required to compile the extension.
Step 4: Configure Vite
Example vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
build: {
// Entry point that registers extension manifests
lib: {
entry: "src/bundle.manifests.ts",
formats: ["es"],
// Output bundle name
fileName: "myproject-backoffice-extensions",
},
// Output directory inside App_Plugins so Umbraco can load it
outDir: "../wwwroot/App_Plugins/MyProjectBackofficeExtensions",
// Clean previous builds
emptyOutDir: true,
sourcemap: true,
// Prevent Umbraco libraries from being bundled
rollupOptions: {
external: [/^@umbraco/],
},
},
});This builds the extension bundle into:
wwwroot/App_Plugins/MyProjectBackofficeExtensions
Step 5: Register the Extension Bundle
Create umbraco-package.json inside Client/public:
{
"id": "MyProject.BackofficeExtensions",
"name": " MyProject.BackofficeExtensions",
"version": "0.0.0",
"allowTelemetry": true,
"extensions": [
{
"name": " MyProject Backoffice Extensions Bundle",
"alias": " MyProject.BackofficeExtensions.Bundle",
"type": "bundle",
"js": "/App_Plugins/MyProjectBackofficeExtensions/myproject-backoffice-extensions.js"
}
]
}This tells Umbraco to load the extension bundle.
Step 6: Add the Entrypoint Manifest
Registers the extension entrypoint. The entrypoint is responsible for bootstrapping the extension when the Umbraco backoffice loads.
Example entrypoint manifest:
export const manifests = [
{
name: "MyProject Backoffice Extensions Entrypoint",
alias: "MyProject.BackofficeExtensions.Entrypoint",
type: "backofficeEntryPoint",
// Dynamically loads the entrypoint module
js: () => import("./entrypoint.js"),
},
];Step 7: Create the Entrypoint
export const onInit = () => { };
export const onUnload = () => { };Step 8: Create the Dashboard Manifest
Example dashboard manifest:
import type { ManifestDashboard } from "@umbraco-cms/backoffice/dashboard";
export const manifests: Array<ManifestDashboard> = [
{
type: "dashboard",
alias: "MyProject.MemberUploadDashboard",
name: "Member Upload Dashboard",
element: () =>
import("./member-upload-dashboard.element.js").then((m) => ({
default: m.MemberUploadDashboard,
})),
weight: 10,
meta: {
label: "Member Upload",
},
conditions: [
{
alias: "Umb.Condition.SectionAlias",
match: "Umb.Section.Members",
},
],
},
];The condition determines which section the dashboard appears in.
Step 9: Build the Dashboard UI
Dashboards are implemented using Web Components and Lit.
Example:
// Import Lit web component base classes
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
// UmbElementMixin allows the component to access Umbraco backoffice services
import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api";
// Notification service used to show success / error messages in the backoffice
import {
UmbNotificationContext,
UMB_NOTIFICATION_CONTEXT
} from "@umbraco-cms/backoffice/notification";
// Register this web component so it can be used in the dashboard manifest
@customElement("member-upload-dashboard")
export class MemberUploadDashboard extends UmbElementMixin(LitElement) {
// Holds the Umbraco notification service instance
#notificationContext?: UmbNotificationContext;
// Stores the currently selected CSV file
@state()
private selectedFile: File | null = null;
// Controls loading state when importing
@state()
private loading = false;
// Number of records processed by the import
@state()
private recordsProcessed: number | null = null;
constructor() {
super();
// Inject the Umbraco notification context
// This allows us to show notifications in the backoffice UI
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (ctx) => {
this.#notificationContext = ctx as UmbNotificationContext;
});
}
/**
* Triggered when the user selects a CSV file
*/
private handleFileChange(e: Event) {
const input = e.target as HTMLInputElement;
this.selectedFile = input.files?.[0] ?? null;
}
/**
* Uploads the CSV file to the backend API
* The API performs the member import
*/
private async upload() {
if (!this.selectedFile) {
this.#notificationContext?.peek("warning", {
data: { message: "Please select a CSV file." }
});
return;
}
this.loading = true;
try {
const formData = new FormData();
formData.append("file", this.selectedFile);
const response = await fetch(
"/umbraco/api/MemberDashboard/UploadFile",
{
method: "POST",
body: formData,
credentials: "include"
}
);
if (!response.ok) throw new Error();
const result = await response.json();
this.recordsProcessed = result.numberOfRecordsProcessed ?? 0;
this.#notificationContext?.peek("positive", {
data: {
message: `${this.recordsProcessed} records processed`
}
});
}
catch {
this.#notificationContext?.peek("danger", {
data: {
message: "There was an error processing the import."
}
});
}
this.loading = false;
}
/**
* Renders the dashboard UI
*/
render() {
return html`
<uui-box headline="Member Import">
<input
type="file"
accept=".csv"
@change=${this.handleFileChange}
/>
<div style="margin-top:15px">
<uui-button
look="primary"
@click=${this.upload}
?disabled=${this.loading}
>
${this.loading ? "Processing..." : "Import"}
</uui-button>
</div>
${this.recordsProcessed !== null
? html`
<p style="margin-top:15px">
${this.recordsProcessed} record(s) processed
</p>
`
: ""}
</uui-box>
`;
}
}
Step 9: Build the Extension
Run the following command in the Client folder of your extension project:
npm run buildAfter building, the bundle will appear in:
wwwroot/App_Plugins/MyProjectBackofficeExtensions
Restart Umbraco and the dashboard will appear in the backoffice.
NOTE
To start the Vite development server in watch mode, run the following command:
npm run watchThe full flow looks like this:
Umbraco starts
↓
umbraco-package.json
↓
myproject-backoffice-extensions.js (bundle)
↓
bundle.manifests.ts
↓
entrypoints + dashboards
↓
extension registry
↓
dashboard manifests
↓
dashboard element
↓
dashboard becomes visible
Key files:
umbraco-package.json – registers the extension bundle
vite.config.ts – builds the extension
bundle.manifests.ts – collects extension manifests
entrypoints/manifest.ts – registers entrypoints
dashboards/*.manifest.ts – registers dashboards
dashboards/*.element.ts – contains the dashboard UI
Key Learnings
The new extension system is modular and scalable.
Manifests separate extension registration from UI logic.
Use bundle.manifests.ts to keep the extension scalable.
Use Umbraco UI components to match the backoffice design.
Final Thoughts
In this guide, we walked through how to create a custom dashboard in Umbraco 17 using the extension template, register it with manifests, and build the UI using Lit. Once you understand this workflow, you can use the same approach to build many other types of backoffice extensions.
If you notice anything that could be improved or have suggestions, feel free to reach out through my socials. Feedback is always welcome and helps make guides like this better for everyone.
For more details, check out the following official Umbraco docs.
Setup Your Development Environment