Generate testable example extensions and run them in the Umbraco backoffice
Generate complete, testable example extensions for the Umbraco backoffice and run them using the Umbraco source's dev infrastructure.
git clone https://github.com/umbraco/Umbraco-CMS
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm install
my-extension/
āāā index.ts # REQUIRED - exports manifests
āāā my-element.ts # Your element(s)
import './my-element.js';
export const manifests = [
{
type: 'dashboard',
alias: 'My.Dashboard',
name: 'My Dashboard',
element: 'my-element',
meta: { label: 'My Dashboard', pathname: 'my-dashboard' },
conditions: [{ alias: 'Umb.Condition.SectionAlias', match: 'Umb.Section.Content' }]
}
];
// my-element.ts
import { LitElement, html, customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('my-element')
export class MyElement extends LitElement {
render() {
return html`<uui-box headline="Hello">It works!</uui-box>`;
}
}
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
VITE_EXAMPLE_PATH=/full/path/to/my-extension VITE_UMBRACO_USE_MSW=on npm run dev
Open http://localhost:5173 - your extension appears in the Content section.
The Umbraco source (Umbraco-CMS/src/Umbraco.Web.UI.Client) provides two ways to load extensions:
npm run example)Examples placed in the examples/ folder inside the Umbraco source.
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm run example
# Select from list of examples
How it works: Sets VITE_EXAMPLE_PATH and imports ./examples/{name}/index.ts
VITE_EXAMPLE_PATH)Extensions from any location on your filesystem - perfect for developing packages.
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
VITE_EXAMPLE_PATH=/path/to/your/extension VITE_UMBRACO_USE_MSW=on npm run dev
How it works:
VITE_UMBRACO_USE_MSW=on mocks the core Umbraco APIsVITE_EXAMPLE_PATH is served through Vite's /@fs/ prefixindex.ts imports <VITE_EXAMPLE_PATH>/index.ts and registers its exports with umbExtensionsRegistry@umbraco-cms/backoffice, lit and @umbraco-ui/uui to the client's copy (avoids duplicate Lit/UUI)Absolute-path support (steps 2 & 4) needs a v18 client built with external-example support. When
running this repo's mocked E2E suites, the Playwright harness injects that support into the client
at UMBRACO_CLIENT_PATH automatically and reverts afterwards ā see the umbraco-mocked-backoffice skill.
// From Umbraco-CMS/src/Umbraco.Web.UI.Client/index.ts
if (import.meta.env.VITE_EXAMPLE_PATH) {
const examplePath = import.meta.env.VITE_EXAMPLE_PATH;
// Absolute paths (external extensions) are served via Vite's /@fs/ prefix; relative paths load from ./
const importPath = examplePath.startsWith('/') ? '/@fs' + examplePath : './' + examplePath;
const js = await import(importPath + '/index.ts');
if (js) {
Object.keys(js).forEach((key) => {
const value = js[key];
if (Array.isArray(value)) {
umbExtensionsRegistry.registerMany(value);
} else if (typeof value === 'object') {
umbExtensionsRegistry.register(value);
}
});
}
}
Key point: Your index.ts must export manifests (arrays or objects) that get registered automatically.
Clone and set up the Umbraco source:
git clone https://github.com/umbraco/Umbraco-CMS
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm install
Your extension needs this minimal structure:
my-extension/
āāā index.ts # Exports manifests array (REQUIRED)
āāā my-element.ts # Your element(s)
āāā my-context.ts # Context (if needed)
āāā package.json # Optional - for IDE support and tests
āāā tsconfig.json # Optional - for IDE support
āāā README.md # Documentation
Your index.ts must export manifests that will be registered:
import './my-dashboard.element.js';
export const manifests = [
{
type: 'dashboard',
alias: 'My.Dashboard',
name: 'My Dashboard',
element: 'my-dashboard',
weight: 100,
meta: {
label: 'My Dashboard',
pathname: 'my-dashboard'
},
conditions: [
{
alias: 'Umb.Condition.SectionAlias',
match: 'Umb.Section.Content'
}
]
}
];
{
"name": "my-extension",
"type": "module",
"devDependencies": {
"@umbraco-cms/backoffice": "^18.0.0",
"typescript": "~5.8.0"
}
}
Important: The @umbraco-cms/backoffice dependency is only for IDE TypeScript support. At runtime, imports are resolved from the main Umbraco project.
cd /path/to/Umbraco-CMS/src/Umbraco.Web.UI.Client
VITE_EXAMPLE_PATH=/absolute/path/to/my-extension VITE_UMBRACO_USE_MSW=on npm run dev
Navigate to http://localhost:5173 - your extension is loaded automatically.
Changes to your extension files trigger hot reload - no restart needed.
// my-dashboard.element.ts
import { LitElement, html, css, customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('my-dashboard')
export class MyDashboardElement extends LitElement {
static override styles = css`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
`;
override render() {
return html`
<uui-box headline="My Extension">
<p>Running in the mocked backoffice!</p>
</uui-box>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'my-dashboard': MyDashboardElement;
}
}
import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { EXAMPLE_MY_CONTEXT } from './my-context.js';
@customElement('example-my-feature-view')
export class ExampleMyFeatureViewElement extends UmbLitElement {
@state()
private _value?: string;
constructor() {
super();
this.consumeContext(EXAMPLE_MY_CONTEXT, (context) => {
this.observe(context.value, (value) => {
this._value = value;
});
});
}
override render() {
return html`
<uui-box headline="My Feature Example">
<p>Current value: ${this._value ?? 'Loading...'}</p>
</uui-box>
`;
}
}
export default ExampleMyFeatureViewElement;
declare global {
interface HTMLElementTagNameMap {
'example-my-feature-view': ExampleMyFeatureViewElement;
}
}
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class ExampleMyContext extends UmbContextBase {
#value = new UmbStringState('initial');
readonly value = this.#value.asObservable();
constructor(host: UmbControllerHost) {
super(host, EXAMPLE_MY_CONTEXT);
}
setValue(value: string) {
this.#value.setValue(value);
}
getValue() {
return this.#value.getValue();
}
public override destroy(): void {
this.#value.destroy();
super.destroy();
}
}
export const EXAMPLE_MY_CONTEXT = new UmbContextToken<ExampleMyContext>(
'ExampleMyContext'
);
export { ExampleMyContext as api };
Add unit tests using @open-wc/testing. See umbraco-unit-testing skill for full setup.
npm install --save-dev @open-wc/testing @web/test-runner @web/test-runner-playwright
Add E2E tests that run against the mocked backoffice. See umbraco-mocked-backoffice skill for patterns.
npm install --save-dev @playwright/test
npx playwright install chromium
Location: ./examples/workspace-feature-toggle/
A complete standalone example demonstrating:
UmbArrayStatecd examples/workspace-feature-toggle
npm install
npm test # Unit tests
npm run test:e2e # E2E tests (requires mocked backoffice running)
Location: Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/
27 official examples covering all extension types. Run any example:
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm run example
# Select from list
| Item | Convention | Example |
|---|---|---|
| Directory | kebab-case describing feature | workspace-context-counter |
| Alias prefix | example. |
example.workspaceView.counter |
| Element prefix | example- |
example-counter-view |
| Context token | EXAMPLE_ + SCREAMING_CASE |
EXAMPLE_COUNTER_CONTEXT |
index.ts exports a manifests arrayVITE_EXAMPLE_PATH is absoluteš¦ Loading external example from: messageImports should use @umbraco-cms/backoffice/*. The Vite plugin resolves these from the main project.
Your extension's node_modules is being used instead of the main project's. The external-example-resolver plugin should handle this, but ensure:
VITE_EXAMPLE_PATH=<abs> VITE_UMBRACO_USE_MSW=on npm run dev@umbraco-cms/backoffice/* not relative paths to node_modulesEnsure the file is within the path specified by VITE_EXAMPLE_PATH. Only files in that directory tree are watched.