Developer’s Guide to Creating a Custom Umbraco 17 Backoffice Dashboard

Published by Debasish Gracias on 20 March 2026

developer-s-guide-to-creating-a-custom-umbraco-17-backoffice-dashboard-banner.png

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

This 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 -ex

This creates the following structure:

MyProject.BackofficeExtensions
│
├─ Client
│   ├─ public
│   │   └─ umbraco-package.json
│   └─ src
│       ├─ dashboards
│       ├─ entrypoints
│       └─ bundle.manifests.ts
└─ vite.config.ts

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

This 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 build

After 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 watch

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

Umbraco Extension Template

Setup Your Development Environment

Vite Package Setup

Creating a Custom Dashboard

Dashboards

Thanks For Reading and Supporting.